position: sticky — это когда блок едет себе спокойно вместе со страницей, но доезжает до определённой точки и цепляется за экран, будто на липучке. Такое поведение отлично подходит для хедеров, боковых меню, шапок таблиц и других элементов, которые должны всегда быть перед глазами.
Сегодня на примерах узнаем, как с этим работать.
Что такое position: sticky в CSS
Основные принципы работы sticky-позиционирования
Обычно элементы располагаются в потоке документа (static по умолчанию) — браузер просто ставит их друг за другом, и каждый занимает своё место. Их можно сравнить с пассажирами в метро, которые сидят в вагоне: никто никого не трогает, каждый на своей позиции.
Но вот если элементу задать другое позиционирование — absolute или fixed, это будет похоже на то, как если бы пассажир встал и ушёл — пустое место тут же займут другие. Браузер рендерит таких «ушедших» отдельно, а потом накладывает их по определённым правилам: те, кто вне потока, толпятся у двери в порядке приоритета своего z-index.
А вот position: sticky — хитрый пассажир. Он сидит как все, но в нужный момент встаёт, берётся за поручень и идёт вдоль сидений (оставаясь в своём вагоне — родительском контейнере). Дойдя до конца, он снова садится на своё место, которое никто не занял, и продолжает путь вместе с остальными.
С точки зрения вёрстки sticky работает так:
- Элемент изначально находится в обычном потоке и занимает своё место в раскладке.
- Ему задают «точку прилипания» — одно из свойств
top,bottom,leftилиright. - При прокрутке, когда выбранный край элемента достигает этой точки (например,
top: 16px), позиционирование переключается в режим фиксации относительно области прокрутки. - Элемент остаётся закреплённым, пока не достигнет границы родительского контейнера — после этого он возвращается в поток и двигается вместе с остальным контентом.
Его место в потоке всегда сохраняется, поэтому раскладка не «скачет». Если же элемент должен перекрывать соседние блоки, это контролируется с помощью z-index.
Отличие sticky от fixed и relative
sticky — особенный режим позиционирования, и понять его проще, если сравнить с другими.
relative
Элемент остаётся в потоке. Можно сместить его с помощью top/left, но место за ним в раскладке сохраняется. При прокрутке он двигается как обычный текст или картинка.
Здесь хедер в позиции relative — движется со всем документом в нормальном потоке:
fixed
Всегда закреплён относительно окна браузера. Не зависит от родителя, выпадает из потока, при прокрутке остаётся на месте. Идеально для шапок, баннеров или кнопок, которые должны всегда висеть в одном месте.
Здесь хедер в позиции fixed — прибит к верхней части:
sticky
Сочетает поведение relative и fixed. Сначала элемент двигается вместе с остальной вёрсткой, как при относительном позиционировании, а в нужный момент фиксируется, как при фиксированном.
В этом примере липкая колонка слева сначала двигается нормально, но как только доходит до верха родительского блока, то прилипает к ней:
Как работает position sticky
Синтаксис и базовое применение
Чтобы элемент «прилип», достаточно прописать ему position: sticky и задать точку прилипания — например, top: 20px. Это значит, что как только верхняя граница элемента окажется в 20 пикселях от верхнего края окна, он перестанет двигаться и замрёт на этой позиции. До этого момента он ведёт себя как обычный блок в потоке.
В CSS выглядит так:
.sidebar {
position: sticky;
top: 20px;
}
Какие элементы можно сделать sticky
Липким можно сделать практически любой блочный элемент: хедер, навигацию, боковое меню, подвал в таблице. Можно даже закрепить отдельную колонку в таблице, чтобы она всегда была на виду при горизонтальной прокрутке.
Sticky имеет смысл применять к тем элементам, которые должны оставаться перед глазами при прокрутке. Например, важные кнопки — да, рекламный баннер, который отвлекает, — лучше не надо.
Область прилипания (sticky-контейнер)
Здесь кроется важный нюанс, о котором часто забывают. Sticky работает только в пределах своего родителя, который участвует в прокрутке. Этот родитель и есть sticky-контейнер. Как только липкий элемент доходит до нижней границы контейнера, он «отлипает» и едет вместе с остальным содержимым.
Например, если у вас есть <aside> внутри колонки, то контейнером для него будет сама колонка. Даже если страница прокручивается дальше, sticky не вылезет за её пределы. Это удобно, если нужно, чтобы сайдбар был закреплён только рядом с конкретным блоком, а не висел над футером или следующими секциями.
Проще всего запомнить так: sticky держится за границы ближайшего прокручиваемого родителя, а не за всю страницу.
Практические примеры использования
Рассмотрим три реальных сценария, где липкое позиционирование экономит нервы и избавляет от сложных костылей на JavaScript.
Закрепление шапки сайта при прокрутке
Классическая задача: при прокрутке хедер остаётся у верхней кромки экрана. Сделать это можно двумя способами — через fixed и через sticky.
С fixed элемент выпадает из потока и живёт поверх остального контента. Ширину он от родителя больше не берёт, поэтому, если хотите растянуть хедер на всю страницу, придётся явно задать left: 0; right: 0; top: 0;. Другой нюанс — контент под ним залезет наверх, так что придётся добавлять padding-top у следующего блока, чтобы ничего не перекрылось.
Пример с fixed:
Если явно не задать размеры, то элемент займёт только необходимую ширину.
А вот в случае sticky элемент всё ещё остаётся в потоке и живёт внутри своего родителя, поэтому ширина автоматически подстраивается под ширину контейнера. Не нужно задавать left/right, достаточно указать точку прилипания по оси прокрутки — обычно это верх:
header {
position: sticky;
top: 0;
}
В отличие от fixed, такой хедер будет липнуть только в пределах своего контейнера. Например, если на странице несколько секций с отдельными заголовками, каждый из них может иметь свой мини-хедер, который закрепляется только в пределах этой секции.
Липкое боковое меню
Боковое меню — другой частый кейс. Вы прокручиваете статью или документацию, а меню с якорями всегда рядом, чтобы быстро перескочить к нужному разделу.
Допустим, у нас есть блок <main>, и внутрь него мы кладём <aside> и основной контент:
<main>
<aside class="sidebar">
<!-- меню -->
</aside>
<div class="content">
<!-- текст статьи -->
</div>
</main>
Чтобы колонки выстроились рядом, а не друг под другом, задаём флекс-раскладку для <main>:
main {
/* включает флексбокс */
display: flex;
/* отступ между колонками */
gap: 20px;
/* чтобы меню не тянулось по высоте контента */
align-items: flex-start;
}
Теперь делаем меню липким:
.sidebar {
position: sticky;
top: 16px;
/* фиксируем ширину колонки с меню */
flex: 0 0 200px;
}
.content {
/* занимает всё оставшееся место */
flex: 1;
}
Свойство top: 16px говорит браузеру: как только верхний край меню окажется в 16 пикселях от верхней границы области прокрутки, закрепи его там.
Меню липнет не ко всему документу, а к своему липкому контейнеру — ближайшему родителю, у которого можно прокручивать контент. В нашем примере это <main>. Пока контейнер выше меню, оно будет висеть у верхнего края экрана. Как только контейнер закончится — меню отлипнет и уедет вместе с остальным контентом.
👉 Важно, чтобы у контейнера, в котором находится меню, не было overflow: hidden или overflow: auto, иначе липкость не сработает как ожидается. Браузер будет считать такой контейнер отдельной зоной прокрутки, и sticky закрепится только внутри него. В итоге элемент либо вообще не залипнет, либо «отлипнет» слишком рано — у границы этого контейнера.
Прилипающие заголовки таблиц
Один из вопросов на собеседованиях: «Как сделать так, чтобы заголовок таблицы всегда был виден при прокрутке вниз и направо?».
Логика такая: здесь мы специально ставим контейнеру с таблицей overflow: auto, чтобы прокрутка была не у всей страницы, а только у таблицы. Тогда sticky будет цепляться за границы именно этого контейнера: шапка прилипнет к его верху и поедет вниз только тогда, когда таблица полностью прокрутится.
<!-- Обёртка со скроллом — это наш липкий контейнер -->
<div class="table-wrapper">
<!-- Внутрь кладём таблицу -->
<table>
<thead>
<tr>
<!-- Первая ячейка пустая — это угол -->
<th></th>
<!-- Остальные th будут липнуть только сверху -->
<th>Колонка 1</th>
<th>Колонка 2</th>
<th>Колонка 3</th>
<th>Колонка 4</th>
<th>Колонка 5</th>
</tr>
</thead>
<tbody>
<!-- Данные неважны, нужны только строки для скролла -->
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<!-- Дальше просто копипастим строки, пока не появится скролл -->
</tbody>
</table>
</div>
Дальше стилизуем эту разметку:
/* Липкий контейнер */
.table-wrapper {
/* ограничиваем высоту, чтобы был вертикальный скролл */
max-height: 300px;
/* включаем прокрутку (и вертикаль, и горизонталь, если нужно) */
overflow: auto;
}
table {
/* убираем лишние зазоры между ячейками */
border-spacing: 0;
/* чтобы при узком контейнере был горизонтальный скролл */
min-width: 600px;
}
/* Заголовки колонок */
thead th {
position: sticky;
/* Прилипаем к верху контейнера */
top: 0;
}
/* Первая колонка — липнем слева при горизонтальном скролле */
thead th:first-child,
tbody td:first-child {
position: sticky;
/* Цепляемся за левую границу контейнера */
left: 0;
}
В результате при прокрутке вниз заголовок таблицы остаётся на месте, а данные под ней двигаются.
Особенности и ограничения
У свойства sticky есть свои ограничения, с которыми можно столкнуться в реальной вёрстке, особенно когда проект сложнее, чем одна колонка и хедер.
Поддержка браузерами и полифиллы
Поддержка position: sticky в современных браузерах почти полная — Chrome, Firefox, Safari, Edge дружат с ним давно.
Проблемы начинаются только в старых версиях Safari на iOS и экзотических браузерах.
Для таких случаев есть JS-полифиллы, которые имитируют липкость через прослушку события scroll: скрипт следит за положением элемента в окне и, когда он доходит до нужной точки, подменяет position: sticky на position: fixed с нужными координатами. При обратной прокрутке элемент «возвращают» в поток.
Но в обычной фронтенд-практике сейчас можно спокойно использовать sticky без опаски, если вы не делаете продукт для древних устройств.
Работа с вложенными sticky-элементами
Иногда в одном контейнере нужно закрепить сразу несколько элементов. Например, у нас есть секция с заголовком и меню. Оба должны залипать, но меню должно появляться чуть ниже заголовка, а не налезать на него.
В таком случае мы делаем один липкий контейнер (сама секция) и два sticky-элемента внутри. Заголовок цепляем за верх (top: 0), а меню смещаем вниз на высоту заголовка (например, top: 40px).
Выглядит это так:
<section class="section">
<header class="section-header">Заголовок секции</header>
<nav class="section-menu">Меню</nav>
<p>...много текста...</p>
</section>
И стилизация:
.section-header {
position: sticky;
top: 0;
}
.section-menu {
position: sticky;
/* прилипает чуть ниже заголовка */
top: 40px;
}
В итоге оба элемента липнут в пределах одной секции и не перекрывают друг друга. Как только секция уедет за экран, заголовок и меню отлипнут вместе с ней.
Взаимодействие с flex и grid
position: sticky спокойно работает в раскладках на flex и grid, но всегда важно помнить: если у контейнера стоит overflow: hidden, auto или scroll — липкость работать не будет.
В разделе про боковое меню мы уже делали липкий элемент во флекс-раскладке — там всё просто: меню залипает внутри своей колонки, а контент прокручивается.
С grid всё ещё проще — липкость работает в любой ячейке. Область прилипания при этом — сама ячейка или её трек (строка или колонка). То есть, если вы закрепили элемент в первом ряду, он будет прилипать в пределах этого ряда.
Допустим, мы хотим, чтобы при прокрутке страницы сверху всегда оставался хедер, а под ним — боковое меню, которое залипает чуть ниже.
<div class="grid">
<header class="header">Хедер</header>
<aside class="sidebar">Меню</aside>
<main class="content">Основной контент.</main>
</div>
Для этого делаем раскладку в две колонки: слева меню, справа контент, а сверху над ними — хедер на всю ширину.
.grid {
/* Делаем грид-контейнером */
display: grid;
/* Определяем схему сетки:
Первая строка — хедер на всю ширину (2 колонки)
Вторая строка — слева меню (sidebar), справа контент */
grid-template-areas:
"header header"
"sidebar content";
/* Ширина колонок:
первая — фиксированная 200px (для меню),
вторая — занимает всё оставшееся место */
grid-template-columns: 200px 1fr;
/* Отступы между ячейками сетки */
gap: 16px;
}
.content {
/* Привязываем элемент к зоне content */
grid-area: content;
}
Затем хедеру и меню задаём липкое позиционирование и разное значение top:
.header {
grid-area: header;
position: sticky;
top: 0;
background: white;
}
.sidebar {
grid-area: sidebar;
position: sticky;
/* чуть ниже хедера */
top: 40px;
background: white;
}
В итоге при прокрутке хедер всегда остаётся вверху, а меню держится чуть ниже него. Как только колонка с меню заканчивается — оно уезжает вместе с остальным контентом.
Решение частых проблем
Sticky ломается чаще всего не из-за багов браузера, а из-за мелочей в вёрстке. Посмотрим, что стоит проверить в первую очередь.
Как задать точку «отлипания»
Чтобы липкий элемент вообще начал работать, ему нужно задать точку прилипания — свойство top (для прилипания сверху) или bottom (для снизу). Без этого браузер не поймёт, где его закрепить.
А уже то, где именно он отлипнет, зависит от нескольких факторов:
- высоты контейнера — в маленьком контейнере отлипнет быстрее, в большом будет липким дольше;
- значения
top / bottom— меняют стартовую точку липкости; - отступов контейнера (
padding-bottom, margin) — создают «воздушную подушку», из-за которой элемент отлипнет раньше.
Например, если у контейнера padding-bottom: 50px, липкий блок остановится за 50px до конца контейнера. Хотим, чтобы лип дольше, — убираем лишние отступы или увеличиваем высоту контейнера.
Почему sticky не работает (основные причины)
Если что-то не клеится, то проверьте, всё ли тут ок:
- Не указана точка прилипания (
top, bottom, leftилиright). Без неё вообще ничего не будет работать. - Родитель прокручивается (
overflow: auto | hidden | scroll). Липкость работает в рамках ближайшего прокручиваемого контейнера. Если он маленький — элемент прилипнет и тут же отлипнет или не прилипнет вообще. - Контейнер ниже липкого элемента. Если элемент не помещается по высоте, липкости не будет.
Совместимость с transform и другими свойствами
Если на родителя липкого элемента повесить что-то типа transform: translateZ(0) или rotate(5deg), браузер создаёт для него свой отдельный слой. Липкость внутри всё ещё будет работать, но могут возникнуть странности:
- Липкий блок вдруг уходит под другие элементы, даже с высоким z-index.
- Пропадают тени (
box-shadow) или фильтры.
То же самое будет со свойствами filter: blur(), perspective, backdrop-filter — все они тоже создают свой слой.
В примере ниже у красного блока с крестиком стоит transform, который создаёт свой контекст наложения. Из-за этого элемент оказывается поверх липкого хедера, хотя в коде идёт ниже. Фикс простой: дать хедеру больший z-index в рамках его слоя.
Если хотите, чтобы липкий элемент всегда был поверх — поднимите ему z-index и проверьте, чтобы у родителя не было overflow: hidden, которое может обрезать «вылезший» кусок.
Бонус для читателей
Скидка 20% на все курсы Практикума до 30 ноября! «Чёрная пятница» и такие скидки бывают раз в год.
Вам слово
Приходите к нам в соцсети поделиться своим мнением о статье и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!
