Webrium
Подписаться

position: sticky не работает — где искать причину

Как диагностировать прилипание элемента при прокрутке и устранить ошибки в разметке или стилях

Элемент пересекает порог прокрутки и фиксируется (position: sticky)Порог прилипанияПрокрутка страницы

Когда position: sticky не работает: два разных симптома

Свойство написано, инспектор показывает его применённым, а position: sticky не работает. Это может означать две разные вещи: либо элемент не прилипает вовсе, либо прилипает, но отпускает раньше, чем нужно. В первом случае проверок три: задан ли порог по оси прокрутки, не стал ли кто-то из предков контейнером прокрутки и не равен ли родитель по высоте самому элементу. Во втором дело в нижнем крае родителя — в том числе родителя, чья высота задана жёстко. Если же объявление в инспекторе зачёркнуто или не применилось вовсе, это вопрос каскада, и он разобран в материале «Стиль не применяется: кто победил в каскаде».

Sticky работает так: элемент остаётся в потоке, как при position: relative, но смещается, когда при прокрутке пересёк бы заданный порог. Смещение ограничено содержащим блоком — обычно это область содержимого родителя. Без порога по оси прокрутки прилипания нет: при вертикальной прокрутке должен быть задан top или bottom, при горизонтальной — left или right. Если все четыре свойства равны auto, элемент ведёт себя как при relative без смещения.

Порог: задан ли он и куда смотрит

Первое, что стоит проверить — порог. Если его нет, элемент не прилипнет. Но даже если порог задан, он может быть направлен не туда. top срабатывает, когда элемент при прокрутке ушёл бы за верхний край области прокрутки, а bottom — только пока элемент лежит ниже её нижнего края. Поэтому bottom у элемента в начале контейнера не даёт видимого эффекта, а у элемента в конце длинного контейнера прижимает его к низу окна.

Порог должен быть задан по той оси, по которой идёт прокрутка. Если прокрутка вертикальная, а задан только left или right, прилипания не будет. Отдельный случай — элемент прилип, но его не видно: его перекрыл сосед. Это не отказ прилипания, и проверки порога, предков и высоты родителя к нему не относятся. Элемент с position: sticky создаёт контекст наложения всегда, прилип он или нет, но при z-index: auto следующие за ним в разметке позиционированные элементы рисуются поверх него, а обычное непозиционированное содержимое — нет. Чтобы прилипшая шапка страницы не перекрывалась, ей задают z-index; подробнее о контекстах наложения — в материале «z-index растёт, а элемент всё равно внизу».

Предок стал контейнером прокрутки: кто и почему

Sticky отсчитывается от ближайшего предка, который стал контейнером прокрутки. Это предок, у которого overflow хотя бы по одной оси равен auto, scroll или hidden — даже если этот предок на деле ничего не прокручивает. Если между окном и элементом стоит такой предок, порог отсчитывается от его области прокрутки, а не от окна. Когда сам предок ничего не прокручивает, при прокрутке окна элемент уезжает вместе с ним: к окну он не прилипнет.

overflow-x со значением hidden, auto или scroll захватывает обе оси: visible по второй оси вычисляется в auto, это видно в getComputedStyle(предок).overflowY. Дальше случаи расходятся. Обёртку с hidden, поставленным «от горизонтальной прокрутки», чинит замена значения — о ней и о её оговорках ниже, в рецептах. Обёртка, которая действительно прокручивает широкое содержимое по горизонтали, перехватывает и вертикаль, и потомок внутри неё к окну не прилипнет. Средствами CSS это не чинится: один и тот же блок не может прокручиваться по горизонтали сам и отдавать вертикаль окну. Обёртке либо задают высоту и прокручивают её в обе стороны, либо пишут скрипт.

html и body ведут себя особым образом. Если overflow задан только на body и при этом у html вычисленное значение остаётся visible, то body не становится контейнером прокрутки, и прилипание работает как ожидалось. Если же значение, отличное от visible, задано на html, то окно просмотра использует это значение, а body становится контейнером прокрутки только в том случае, когда у него самого тоже задано значение, отличное от visible.

Чтобы найти виновного предка, обходите их в консоли, начиная с родителя прилипающего элемента. Проверяйте только те узлы, которые формируют собственный блок — пропускайте предков с display: contents или строчных (display: inline). Вот код для обхода:

const root = document.documentElement;
const rootStyle = getComputedStyle(root);
const rootVisible = rootStyle.overflowX === 'visible' && rootStyle.overflowY === 'visible';
const scrolls = (value) => value === 'auto' || value === 'scroll' || value === 'hidden';

let node = document.querySelector('.sticky-element').parentElement;
let culprit = null;
while (node && node !== root) {
  const style = getComputedStyle(node);
  const noBox = style.display === 'contents' || style.display === 'inline';
  const movedToViewport = node === document.body && rootVisible;
  if (!noBox && !movedToViewport && (scrolls(style.overflowX) || scrolls(style.overflowY))) {
    culprit = node;
    break;
  }
  node = node.parentElement;
}
if (culprit) {
  const style = getComputedStyle(culprit);
  console.log('Контейнер прокрутки:', culprit, style.overflowX, style.overflowY);
} else {
  console.log('Между элементом и окном контейнера прокрутки нет');
}

Этот код пропускает body, если у html вычисленный overflow равен visible, и останавливается на первом найденном контейнере прокрутки. Он также игнорирует предков без собственного блока.

Если предок действительно прокручивается — например, у него ограничена высота, — элемент прилипнет относительно его области прокрутки, а не окна. Это не поломка, а особенность работы sticky.

Родителю некуда отпускать элемент

Sticky держится, пока родитель виден, и уезжает вместе с его нижним краем. Если родитель по высоте равен самому элементу, двигаться некуда. Это выглядит как неработающее свойство.

Во флексе два противоположных случая. Если прилипающий элемент сам является флекс-элементом в контейнере со строчным направлением, по умолчанию он растянут на всю высоту строки. Если контейнер по высоте равен элементу, прилипания не будет. Лечится это выравниванием элемента по началу строки: align-self: flex-start.

Если прилипающий элемент лежит внутри флекс-элемента-обёртки, проблема в отсутствии растяжения. При align-items: flex-start или center у контейнера обёртка сжимается по содержимому, и прилипания тоже не будет. Лечится возвратом растяжения обёртке. О выборе между двумя моделями раскладки — в материале «Flexbox или Grid: как выбрать раскладку».

В сетке боковая колонка, область которой занимает всю высоту грид-контейнера, в растянутом виде не прилипает. Исправляется align-self: start.

Прилипает, но отпускает раньше времени

Если элемент прилипает, но отпускает раньше ожидаемого, проверьте высоту родителя. Жёсткая высота мешает, когда она меньше содержимого и задана самому родителю либо досталась ему по цепочке height в процентах. Например, прилипающий элемент лежит прямо в body при height на html и body или прямо в обёртке высотой в экран. Содержимое вытекает за нижний край родителя, а sticky держится только до этого края. Симптом — «прилипает на первом экране и уезжает». Лечится заменой height на min-height у родителя.

Если между предком с жёсткой высотой и элементом стоит родитель с высотой по содержимому, прилипание работает на всю высоту этого родителя, и жёсткая высота предка ни при чём.

Несколько sticky-элементов с одинаковым порогом в одном содержащем блоке прилипают к одной линии и накладываются друг на друга. Чтобы они вставали стопкой, каждому следующему задают порог, равный суммарной высоте предыдущих. Заголовки, лежащие каждый в своей секции, выталкивают друг друга: предыдущий уезжает вместе с нижним краем своей секции. Это одно и то же правило границы родителя.

Элемент выше области прокрутки при заданном top прилипает верхним краем, и его нижняя часть остаётся недоступной, пока не кончится родитель. Решается ограничением высоты такого элемента и собственной прокруткой внутри него либо отказом от прилипания.

Чем чинить: рецепты по причинам

Если предок стал контейнером прокрутки, замените overflow: hidden на overflow: clip. Но учтите три оговорки: clip по одной оси и hidden по другой вычисляется в hidden, предок теряет контекст форматирования, и программная прокрутка содержимого становится невозможной. В устаревшем браузере clip не распознаётся и отбрасывается — обрезка пропадает.

Если проблема в растяжении во флексе, верните обёртке растяжение или выровняйте прилипающий элемент по началу строки. В сетке для боковой колонки используйте align-self: start.

Если родителю задана жёсткая высота, замените height на min-height. Если причина в переполнении, ради которого ставили overflow, уберите саму причину переполнения.

Если ни один из этих способов не подходит, перенесите элемент на уровень выше, к родителю нужной высоты.

Тупик: что пробуют первым и почему это не помогает

Первым делом обычно меняют сам элемент: поднимают z-index, подбирают другое значение уже заданного порога, дописывают !important. Но z-index управляет перекрытием, а не положением — если position: sticky не работает, проблема не в перекрытии. Объявление и так применено, а другая величина порога сдвигает линию прилипания, но не меняет того, относительно чего и в каких границах элемент движется.

Следом переписывают на position: fixed со слушателем прокрутки. Элемент выпадает из потока, пустое место под него приходится держать вручную, и самим считать, где прилипание кончается. Причина в предке остаётся, а fixed имеет свою ловушку: предок с transform и подобными свойствами становится для него содержащим блоком. Sticky же не зависит от этих свойств предка — он привязан к контейнеру прокрутки и ограничен содержащим блоком.

Позиция редакции

Неработающий sticky диагностируется подъёмом по цепочке предков, а не правкой элемента. Сначала проверьте overflow у каждого предка, начиная с родителя прилипающего элемента, затем высоту родителя — не равна ли она высоте самого элемента и не задана ли жёстко, ему самому или по цепочке height в процентах от предков.

Привычку ставить overflow: hidden на обёртки стоит заменить на overflow: clip с оговорками или убрать причину переполнения.

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

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Вложенный элемент с z-index 9999 оказывается ниже из-за контекста предка с z-index 19999 внутри 1
Углубление

z-index растёт, а элемент всё равно внизу

Почему элемент с большим z-index всё равно рисуется под соседом: числа сравниваются только внутри одного контекста наложения. И как, поднимаясь по разметке, найти предка, который этот контекст создал.

Сравнение одномерной и двумерной раскладки: Flexbox и Grid как разделённые части одного поляFlexbox (одна ось)Grid (две оси)
Предыстория

Flexbox или Grid: как выбрать раскладку

Как выбрать между Flexbox и Grid для типовых элементов интерфейса и где в одном компоненте проходит граница между двумя моделями раскладки.

Из нескольких CSS-правил применяется только одно, остальные отфильтрованы каскадомПравила CSSПрименено
Практика

Стиль не применяется: кто победил в каскаде

Как найти и исправить конфликты в CSS-каскаде, из-за которых стили игнорируются или переопределяются.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

↑↓ выбрать · Enter открыть · Esc закрыть