Как работает position: sticky и почему он часто не прилипает

от автора

Привет, Хабр!

position: sticky — штука, которая превращает relative-элемент в fixed-элемент, как только он доезжает до заданного инсет-порога, и отлипает в момент, когда скроллинг выталкивает родителя за край.

Работает круто, пока вы не включите overflow, не забудете задать top, не положите элемент в flex c align-items: stretch, не сделаете таблицу из <thead> и не упрётесь в кейс с вложенными скролл-контейнерами.

Что такое position: sticky и как движок принимает решение, липнуть ли элементу

Браузер хранит для каждого sticky-бокса sticky-constraint rectangle. Это прямоугольник, ограниченный ближайшим scroll container (предком с overflowvisible) и инсет-значениями (top/bottom/left/right). Пока граница прямоугольника не пересекла вьюпорт, элемент ведёт себя как position: relative; после пересечения — переключается в fixed и пинится относительно того же контейнера, а не всего окна.

.sticky {   position: sticky;   top: 0;            /* ← без этого инсет-триггера sticky не сработает */   z-index: 10;       /* создаём новый stacking context, чтобы перекрывать контент */ }

Большинство не-прилипаний диагностируется одной строчкой — outline: 1px solid red; на подозреваемом контейнере. Обводка сразу показывает, к какому прямоугольнику элемент привязан.

Условия, без которых sticky не оживёт

Условие

Как проверить

Как починить

1

Инсет задан (top/left/...)

DevTools > Computed > Position

Добавьте top (или bottom/…) хотя бы 1px

2

Родитель не создаёт скролл-контейнер

Ищите overflow: hidden/auto/scroll/clip у предков

Уберите overflow, замените hidden → clip, или вынесите sticky наружу

3

Высота родителя известна

В Layout панели у div высота — auto?

Задайте height или min-height

4

Sticky меньше родителя

Сравните bounding-rect в консоли

Ограничьте max-height, включите overflow на самом sticky

5

Flex/Grid не ломает выравнивание

flex-контейнер со align-items: stretch?

Поставьте align-self: flex-start на sticky

overflow: hidden создаёт невидимый scroll-context и ломает прилипания; спасение — overflow: clip или вынос sticky за пределы контейнера.

Что ж оно опять не прилипает? — короткий чек-лист:

// Вставьте в консоль DevTools [...document.querySelectorAll('.sticky')].forEach(el => {   const chain = [];   let p = el.parentElement;   while (p) { chain.push([p.tagName, getComputedStyle(p).overflow]); p = p.parentElement; }   console.table(chain, ['0','1']); // ищем overflow ≠ visible });

Overflow: враг липучести

<div class="wrapper">   <aside class="sidebar sticky">…</aside>   <main>…длинный лонгрид…</main> </div>
.wrapper {   overflow-x: hidden;      /* Казалось бы, просто убираем горизонтальный скролл… */ }  .sidebar {                  /* …но sticky больше не работает */   position: sticky;   top: 0; }

Почему падает? overflow-x: hidden автоматически ставит overflow-y: auto. Браузер считает .wrapper новым scroll-container, и для него sticky порог никогда не достигается.

Фикс:

.wrapper { overflow: clip; }   /* скрываем «мусор», но не создаём скролл */

или

/* Выносим липкую часть за «скрывающий» контейнер */ .layout {   display: grid;   grid-template-columns: 280px 1fr; }

Edge-кейсы

Таблицы

Есть спецификационная дыра: thead и tr не умеют position: relative, поэтому sticky нужно вешать напрямую на th.

<table>   <thead>     <tr>       <th class="sticky">#</th>       <th class="sticky">Название</th>       …     </tr>   </thead> </table>  th.sticky {   position: sticky;   top: 0;   background: var(--bg); }

Если нужна залипающая первая колонка — комбинируйте position: sticky + left: 0 на каждом td/th целевого столбца.

Flexbox

align-items: stretch заставляет строку растянуться по высоте контента, и sticky просто не остаётся области, где прилипнуть. Решение — притянуть элемент к верху через align-self = flex-start и дать контейнеру фиксированную высоту.

.column {   display: flex;   flex-direction: column;   height: 100vh;          /* ← важно */ }  .sidebar {   align-self: flex-start;   position: sticky;   top: 0; }

Вложенные скролл-контейнеры

Когда sticky сидит внутри скролла в скролле, он приклеивается к ближайшему контейнеру. Часто приходится выбирать, где именно он должен зависнуть, и передавать событие прокрутки наружу. Быстрый костыль — связать скроллы через JS:

outer.addEventListener('scroll', e => inner.scrollLeft = e.target.scrollLeft);

Sticky vs Fixed: когда что брать в продакшен

Критерий

position: sticky

position: fixed

Блок «отлипает» с родителем

+

Не блокирует событие scroll

+

Не создаёт новый контекст наложения без z-index

+

-, fixed всегда новый stacking context

Поддержка IE 11

+

Требует указать inset

+

Необязательно

Fixed — это глобальная привязка к viewport. Sticky — контекстная, и в SPA-верстке это часто спасает от ручного расчёта высот.

Архитектура вертикальных срезов: как она помогает со sticky

Фронтенд больших продуктов давно перестал быть «шаблоном + немного jQuery». У каждой фичи — собственный state-менеджмент, micro-layouts и огороды из overflow. Если лепить sticky поверх слоёв, получаем хрупкие зависимости. Можно использовать Vertical Slice Architecture: ковыряемся не по слоям (controller → service → repo), а по фичам — slice на каждый use-case.

Что это даёт для sticky?

  1. Изолированный scroll-context. У каждого slice свой layout-root, можно смело применять overflow и не бояться поломать чужие sticky-шапки.

  2. Предсказуемая область прилипания. Slice знает, кто его родитель, и может гарантировать высоту контейнера.

  3. Тестируемость. В E2E-тестах проще проверить, что getBoundingClientRect().top === 0, когда slice развёрнут.

Мини-пример на React + CSS Modules

// slices/article/ArticlePage.tsx export default function ArticlePage() {   return (     <section className={s.slice}>       <TableOfContent className={s.toc}/>       <article>{/* markdown */}</article>     </section>   ); }
/* slices/article/article.module.css */ .slice {   display: grid;   grid-template-columns: 240px 1fr;   height: 100vh;          /* ← высота замкнута внутри среза */ }  .toc {   position: sticky;   top: calc(var(--header-h) + 16px);   align-self: start;      /* flex/grid safety */ }

Slice держит свои размеры и не зависит от глобального .wrapper{overflow-x:hidden} в другом модуле. Если захотим модалку поверх контента, просто создаём ещё один slice и не трогаем существующий.

Шпаргалка по Sticky

  1. Проверяем тройку: top, overflow, height. 90 % багов тут.

  2. Смотрим размер sticky против контейнера. Если больше — не пристанет.

  3. В flex/grid дробим ось выравнивания. align-self / justify-self.

  4. Нужен скролл-замок? overflow: clip вместо hidden.

  5. Поддержка Safari ≤ 12? Двойная декларация position: -webkit-sticky; position: sticky;.

  6. Сложный UI? Укладываем в Vertical Slice > изолируем layout.

Используйте вертикальные срезы, чтобы держать сложные интерфейсы под контролем, и не бойтесь комбинировать sticky с таблицами, flex’ом и grid’ом.


Если вы дочитали статью до конца — вы точно понимаете, что современная верстка это уже не «два div»а и float:left». Это архитектура компонентов, взаимодействие с движком браузера, баги из-за overflow и точечная отладка через DevTools.

Если вы хотите:

  • разобраться, почему CSS ведет себя «странно» — и как на самом деле работает модель раскладки;

  • научиться писать адаптивную, поддерживаемую верстку, а не просто «чтоб выглядело как в макете»;

то рекомендуем ознакомиться с программой курса HTML/CSS, который стартует 25 июня.


ссылка на оригинал статьи https://habr.com/ru/articles/912012/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *