Position Sticky в CSS: полное руководство по закреплению элементов на странице

Залипательное объяснение с примерами

Position Sticky в CSS: полное руководство по закреплению элементов на странице

position: sticky — это когда блок едет себе спокойно вместе со страницей, но доезжает до определённой точки и цепляется за экран, будто на липучке. Такое поведение отлично подходит для хедеров, боковых меню, шапок таблиц и других элементов, которые должны всегда быть перед глазами.

Сегодня на примерах узнаем, как с этим работать.

Что такое position: sticky в CSS

Основные принципы работы sticky-позиционирования

Обычно элементы располагаются в потоке документа (static по умолчанию) — браузер просто ставит их друг за другом, и каждый занимает своё место. Их можно сравнить с пассажирами в метро, которые сидят в вагоне: никто никого не трогает, каждый на своей позиции.

Но вот если элементу задать другое позиционирование — absolute или fixed, это будет похоже на то, как если бы пассажир встал и ушёл — пустое место тут же займут другие. Браузер рендерит таких «ушедших» отдельно, а потом накладывает их по определённым правилам: те, кто вне потока, толпятся у двери в порядке приоритета своего z-index.

А вот position: sticky — хитрый пассажир. Он сидит как все, но в нужный момент встаёт, берётся за поручень и идёт вдоль сидений (оставаясь в своём вагоне — родительском контейнере). Дойдя до конца, он снова садится на своё место, которое никто не занял, и продолжает путь вместе с остальными.

С точки зрения вёрстки sticky работает так:

  1. Элемент изначально находится в обычном потоке и занимает своё место в раскладке.
  2. Ему задают «точку прилипания» — одно из свойств top, bottom, left или right.
  3. При прокрутке, когда выбранный край элемента достигает этой точки (например, top: 16px), позиционирование переключается в режим фиксации относительно области прокрутки.
  4. Элемент остаётся закреплённым, пока не достигнет границы родительского контейнера — после этого он возвращается в поток и двигается вместе с остальным контентом.

Его место в потоке всегда сохраняется, поэтому раскладка не «скачет». Если же элемент должен перекрывать соседние блоки, это контролируется с помощью z-index.

Источник: abouolia.github.io

Отличие 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 ноября! «Чёрная пятница» и такие скидки бывают раз в год.

Вам слово

Приходите к нам в соцсети поделиться своим мнением о статье и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!

Вам может быть интересно
easy
[anycomment]
Exit mobile version