Фиксированные заголовки – это распространенный шаблон пользовательского интерфейса, но часто им нужно менять свой внешний вид, когда они становятся "закрепленными", чтобы сохранить читаемость или сэкономить место. Традиционные решения требовали прослушивателей прокрутки JavaScript, что могло вызвать проблемы с производительностью. Современный CSS позволяет обрабатывать это нативно с помощью запросов состояния прокрутки.
Руководство по настройке
1. Создайте плавающий контейнер
Вам нужен контейнер, который будет действовать как фиксированный элемент и контейнер для запроса состояния прокрутки.
<!-- Wrap in a section to define the containment area for the sticky element -->
<div class="section">
<div class="sticky-container">
<div class="sticky-header">
Section Header
</div>
</div>
<div class="content">
<!-- Content goes here -->
</div>
</div>
2. Примените CSS для закрепления и типа контейнера
Задайте container-type: scroll-state для элемента position: sticky, а не для его прокручиваемого предка. Всегда указывайте container-name, поскольку запросы могут быть вложенными, а контейнеры без названий будут конфликтовать. Вы также можете комбинировать его с запросами размера, например container-type: scroll-state inline-size.
.sticky-container {
position: sticky;
top: 0;
/* MANDATORY: Always specify a container name to avoid collisions if CQs are nested */
container-type: scroll-state;
container-name: section-header;
z-index: 10;
}
.sticky-header {
/* Base styles for the header */
background: #f0f0f0;
padding: 20px;
transition: background-color 0.3s, box-shadow 0.3s;
}
Важно! Состояние прокрутки запрашивается потомками контейнера состояния прокрутки. Стили в запросе @container будут применяться не к самому элементу .sticky-container, а к его дочерним элементам (например, .sticky-header). Вы не можете задать стиль для самого элемента контейнера с помощью его собственного запроса scroll-state.
3. Таргетинг на состояние зависания
Используйте запрос @container scroll-state(...), чтобы применить стили, когда заголовок закреплен.
/* Apply styles when the named container is stuck at the top */
@container section-header scroll-state(stuck: top) {
.sticky-header {
background: #0056b3;
color: white;
box-shadow: 0 4px 6px rgb(0 0 0 / 0.15);
}
}
Совет. Вы также можете использовать логические свойства, такие как stuck: inset-block-start или stuck: inset-inline-start, чтобы улучшить поддержку интернационализации (i18n) и макетов с направлением письма справа налево (RTL). Для этого запрашивайте края, связанные с потоком, а не физические.
Примечания об изменениях параметров
Если не отключить привязку прокрутки, изменение свойств, влияющих на макет (высота, отступы, размер шрифта), при закреплении может вызвать визуальное мерцание. Привязка прокрутки к контенту под фиксированным элементом корректирует смещение прокрутки, чтобы компенсировать изменение макета. Это выталкивает элемент из фиксированного положения и вызывает колебания.
Чтобы избежать этого, отключите привязку прокрутки на родительском элементе фиксированного элемента:
.section {
overflow-anchor: none;
}
Примените overflow-anchor: none к непосредственному родительскому элементу position: sticky, а не к :root или body, иначе привязка прокрутки будет отключена для всей страницы. Благодаря этому вы можете свободно изменять любое свойство в застрявшем состоянии, включая свойства блочной модели и преобразования.
НЕ ИСПОЛЬЗУЙТЕ overflow-anchor: none для элементов, закрепленных внизу. Фиксация прокрутки компенсирует только смещения макета выше текущей позиции прокрутки, поэтому она не влияет на элементы, закрепленные внизу. Не изменяйте свойства блочной модели в закрепленном состоянии для нижних закрепленных заголовков.
Резервные стратегии
Запросы состояния прокрутки – это прогрессивное улучшение. В браузерах, которые не поддерживают container-type: scroll-state, заголовок будет оставаться вверху (благодаря position: sticky), но не будет визуально трансформироваться. В большинстве случаев рекомендуется использовать именно этот подход.
Если закрепление не является обязательным, но position: sticky без другого стиля будет выглядеть хуже, чем без закрепления, вы можете обернуть position: sticky и связанное объявление в запрос @supports:
.sticky-container {
@supports (container-type: scroll-state) {
position: sticky;
top: 0;
container-type: scroll-state;
container-name: section-header;
z-index: 10;
}
}
Совет. Если для читаемости закрепленного элемента требуется другой цвет фона, настройте макет так, чтобы стиль по умолчанию работал везде (прогрессивное улучшение). Если вам необходимо использовать резервный вариант, вы можете ограничить поведение position: sticky внутри запроса @supports (container-type: scroll-state).
Если визуальное преобразование абсолютно критично для дизайна (например, закрепленное состояние вводит фон или уплотнение, без которых контент нечитаем), вы можете реализовать надежный резервный вариант JavaScript с помощью IntersectionObserver. Вам нужно продублировать стили CSS в классе .is-stuck. Следующий общий полифил проверяет getComputedStyle, чтобы найти правильный контейнер прокрутки (по умолчанию область просмотра), соответствующий поведению @container scroll-state(stuck: top):
function getScrollParent(node) {
if (node == null || node === document.body || node === document.documentElement) {
return null; // default to viewport
}
if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
const overflow = getComputedStyle(node).overflow;
if (overflow !== 'visible' && overflow !== 'clip') {
return node;
}
}
return getScrollParent(node.parentNode);
}
document.querySelectorAll('.sticky-container').forEach(container => {
const root = getScrollParent(container);
const topOffset = parseFloat(getComputedStyle(container).top) || 0;
const observer = new IntersectionObserver(
([e]) => {
// Toggle the fallback class on the sticky header container
e.target.classList.toggle('is-stuck', e.intersectionRatio < 1);
},
{
root: root,
threshold: [1],
rootMargin: `-${topOffset + 1}px 0px 0px 0px`
}
);
observer.observe(container);
});
Примечание. Этот общий шаблон IntersectionObserver также можно использовать в качестве полифила для запроса scroll-state(scrollable).