Управление прокруткой с помощью CSS Scroll Snap Module Level 1
Практически все веб-интерфейсы используют плавную прокрутку для создания эффекта смены страниц, каруселей, галерей или длинных лендингов с секциями. Однако стандартное поведение скролла не всегда позволяет точно остановиться на нужном элементе — пользователь может случайно остановиться между секциями, оставив элемент частично видимым. Спецификация CSS Scroll Snap Module Level 1 решает эту проблему, позволяя разработчикам задавать «точки привязки» (snap positions), к которым «прилипает» прокрутка после завершения скролла.
Что такое CSS Scroll Snap?
Scroll Snap — это набор CSS-свойств, которые управляют поведением прокрутки в scroll-контейнере. Они позволяют «привязывать» позицию прокрутки к определённым элементам или выравниваниям. Разработчик не управляет каждым пикселем прокрутки вручную, а задаёт набор допустимых позиций, возле которых браузер может корректировать финальную остановку скролла. Это полезно для:
- постраничной прокрутки (pagination);
- горизонтальных галерей изображений;
- вертикальных лендингов с секциями на весь экран;
- любого интерфейса, где важно точно позиционировать прокручиваемый контент.
Модуль находится на стадии Candidate Recommendation Snapshot и достаточно хорошо поддерживается современными браузерами (включая мобильные). Он не заменяет существующие механизмы скролла, а дополняет их.
Основные понятия
Прежде чем перейти к свойствам, выстроим терминологию.
Все слайдеры специально сделаны вертикальными, чтобы удобнее работать с колёсиком мышки, но это также будет работать для стандартных горизонтальных слайдеров. Чтобы лучше ощутить понятия - скрольте примеры =)
1) Сцена: scroll container
Это элемент с реальной прокруткой (overflow: auto|scroll). Именно этот контейнер участвует в snapping.
Основные свойства scroll container:
- Именно здесь прокручивается весь контент внутри;
- Именно этот контейнер потом «ловит» snap-позиции, о которых узнаете чуть позже.
<div class="gallery">
<div class="slide">1</div>
<div class="slide">2</div>
<div class="slide">3</div>
<div class="slide">4</div>
</div>
.gallery {
overflow: auto; /* без этого scroll container не появляется */
}
2) Видимая часть: scrollport
scrollport — это видимая область scroll container, то есть та часть контейнера, через которую пользователь видит прокручиваемый контент.
Основные свойства:
- это «окно» (viewport) внутри контейнера;
- показывает только часть содержимого, остальное находится за пределами и доступно через прокрутку;
- используется как базовая область для расчётов прокрутки и snap.
3) Смещение области: scroll-padding
CSS-свойство scroll-padding задаёт внутренние смещения для scrollport и тем самым определяет optimal viewing region — область, в которую браузер старается приводить целевые элементы.
Свойство не меняет layout, но меняет геометрию целевой области для прокрутки. Это значит, что слайды внутри слайдера никак не изменят свои размеры из-за указанного scroll-padding.
Основные свойства scroll-padding:
- задаёт отступы внутрь
scrollportи «сужает» рабочую область навигации; - в snap-контейнере эта область становится
snapport; - проценты считаются от размеров
scrollport; - значение
autoоставляет расчёт браузеру (обычно эквивалентно0px).
Отдельно полезно помнить поведение корневого элемента из спецификации: если scroll-padding задан на root-элементе документа, он применяется к viewport, а не «переезжает» с body. То есть для управления snap-геометрией всей страницы ориентироваться нужно именно на корневой scrolling box.
4) snapport
snapport — это рабочая область внутри scrollport, которая получается после применения scroll-padding.
Формула для понимания: snapport = scrollport - scroll-padding. В третьем примере, который чуть выше, мы получили «область после scroll-padding» — это и есть snapport.
Без scroll-padding: snapport = scrollport
Основные свойства snapport:
- размер и положение
snapportнапрямую зависят отscroll-padding.
Про эти свойства, про которые чуть позже:
- используется как опорная область для
scroll-snap-align; - именно относительно
snapportбраузер вычисляет snap positions;
5) Сторона элемента: scroll snap area
Теперь перейдём к элементам. scroll snap area - область элемента, которую браузер использует для snap-выравнивания.
Если упрощать, то:
scroll snap area = border box элемента
Но точнее по спецификации всё немного интереснее: за основу берётся transformed border box, затем браузер строит его axis-aligned bounding box в координатах scroll container, и уже от этой области дальше считает snap area.
Основные механизмы scroll snap area:
- в простом объяснении базой можно считать
border boxэлемента; - именно эта область участвует в вычислении
snap position; - итоговое выравнивание зависит от пары:
scroll snap area+snapport.
6) Изменение области span area: scroll-margin
CSS-свойство scroll-margin расширяет snap area наружу и влияет на расчёт snap/scrollTo/якорей.
snap area = border box элемента + scroll-margin
То есть scroll-margin не меняет размер самого элемента в layout, а только расширяет область, которую браузер использует для snap-выравнивания.
Итоговый пример (scroll-margin: 20px 0 у активного слайда):
7) Правило выравнивания: scroll-snap-align
Вот здесь наконец складываются все предыдущие детали:
- у контейнера уже есть
snapport; - у элемента уже есть
snap area; - осталось только указать, как именно их выровнять друг относительно друга.
Именно это и делает scroll-snap-align. В спецификации свойство определяется как выравнивание snap area элемента внутри snapport контейнера. Это не «включатель snapping», а правило: какая линия элемента должна совпасть с какой линией snapport.
scroll-snap-align: [ none | start | end | center ]{1,2};
start— совместить началоsnap areaс началомsnapport;center— совместить центры;end— совместить концы;none— этот элемент не создаёт snap position по указанной оси.
Если указано одно значение, оно применяется к обеим осям. Если два, то первое задаёт выравнивание по block axis, а второе по inline axis. Это важный момент из спецификации: порядок здесь логический, а не «сначала X, потом Y».
Для наглядности я буду использовать неофициальный термин snap-line. Это воображаемая линия start, center или end у snapport и у snap area. Когда такие линии совпадают, получается snap position.
Чтобы увидеть это буквально, сначала разложим обе стороны по одной горизонтальной оси. Здесь схема специально упрощена: у snap area есть начало, центр и конец, и у snapport есть такие же три опорные линии.
snap area
линии самого элемента
У snap area браузер может ориентироваться на три линии: start, center и end.
snapport
линии области, внутри которой идёт выравнивание
У snapport есть такой же набор линий: start, center и end.
Итого: scroll-snap-align просто выбирает, какая линия snap area должна совпасть с такой же линией snapport: start ↔ start, center ↔ center или end ↔ end.
Теперь можно показать три базовых варианта:
scroll-snap-align: startstart ↔ start: верхняя линия snap area совпала с верхней линией snapport.
scroll-snap-align: centercenter ↔ center: центр snap area совпал с центром snapport.
scroll-snap-align: endend ↔ end: нижняя линия snap area совпала с нижней линией snapport.
8) snap position
Теперь можно назвать кульминационный термин. Спецификация определяет snap position как scroll position, которая удовлетворяет заданному выравниванию snap area внутри snapport.
Проще говоря:
snap area— это область элемента;scroll-snap-align— это правило выравнивания;snap position— это уже конкретное значение прокрутки, при котором правило выполнено.
Удобная формула для запоминания:
snap area + snapport + правило align = snap position
Ниже один и тот же элемент в двух состояниях. Слева линии ещё не совпали, справа совпали. В этот момент текущий scroll offset контейнера и есть snap position.
Именно поэтому полезно разделять термины:
scroll-snap-alignотвечает на вопрос «как выравнивать?»;snap positionотвечает на вопрос «при каком offset это выравнивание выполнено?».
9) snap positions
У контейнера почти никогда нет только одной возможной позиции привязки. Обычно у него есть набор кандидатов: каждый элемент со своим scroll-snap-align добавляет одну или несколько возможных snap positions по осям, а браузер затем выбирает, к какой из них притянуться.
То есть:
snap position— одна конкретная позиция;snap positions— весь набор допустимых позиций-кандидатов в контейнере.
Ниже длинная лента карточек. У каждой карточки есть своя потенциальная точка остановки, а синий viewport показывает, что браузер в каждый момент выбирает одну из них как финальную.
Именно этот набор браузер рассматривает дальше, когда применяет правила выбора из раздела Choosing Snap Positions в спецификации: обычно он старается подобрать ближайший уместный кандидат, но точный алгоритм остаётся на стороне user agent.
10) Процесс: snapping
После естественного end-point браузер может довести прокрутку до ближайшей уместной snap-позиции.
Здесь важно разделить два шага:
- Пользователь скроллит контейнер колесом, тачпадом, свайпом или клавиатурой.
- Браузер получает естественную точку остановки прокрутки и уже после этого решает, нужно ли чуть скорректировать её до ближайшей подходящей
snap position.
Эта естественная точка остановки и есть natural end-point. Она появляется из обычной физики скролла: инерции, скорости жеста, текущего scroll offset и поведения платформы. То есть сначала контейнер почти «доезжает сам», а уже потом Scroll Snap проверяет, не стоит ли закончить движение в более аккуратной позиции.
Если рядом есть подходящий кандидат, браузер может изменить финальный scroll offset так, чтобы он удовлетворял правилу выравнивания. Именно этот короткий переход от естественной остановки к snap-позиции и называется snapping.
Проще говоря:
natural end-point— место, где прокрутка закончилась бы без snap;snapped position— место, куда браузер решил довести контейнер после проверки snap-правил;snapping— сам процесс этого доведения.
На схеме серой линией отмечен natural end-point, красной — итоговая snapped position, а синяя стрелка показывает, что браузер может немного «дотянуть» прокрутку до более подходящего кандидата.
При этом важно, что браузер не обязан слепо выбирать абсолютно ближайшую точку в любой ситуации. Он учитывает ось, строгость (mandatory или proximity), направление движения, достижимость позиции и другие правила выбора из спецификации. Поэтому snapping лучше понимать не как «мгновенный прыжок к ближайшей карточке», а как корректировку финальной остановки по правилам Scroll Snap.
11) Включатель режима: scroll-snap-type
До этого мы разобрали геометрию snapping: у контейнера есть snapport, у карточек есть snap area, у них есть правила scroll-snap-align, а браузер уже умеет вычислять набор snap positions.
Но всё это ещё не означает, что контейнер вообще обязан использовать snapping. Именно scroll-snap-type включает этот режим на scroll-контейнере и отвечает сразу на два вопроса:
- по какой оси искать кандидатов на привязку;
- с какой строгостью доводить прокрутку до ближайшей
snap position.
scroll-snap-type:
none |
[ x | y | block | inline | both ]
[ mandatory | proximity ]?;
Если коротко, scroll-snap-type - это переключатель режима для контейнера:
x,y,inline,block,both— говорят, где именно искать snap positions;mandatoryилиproximity— говорят, насколько настойчиво к ним притягиваться.
Без scroll-snap-type даже карточки с scroll-snap-align не будут снапиться: геометрия уже описана, но сам режим ещё не включён.
Для вертикальных секций и экранов чаще всего нужен y: контейнер ищет snap positions сверху вниз.
По строгости есть два основных режима:
mandatoryзаставляет контейнер закончить прокрутку на одной изsnap positions;proximityснапит мягче и только если естественная остановка уже достаточно близко к подходящей позиции.
Ниже слайдер: контейнер включает режим y mandatory, а карточки отдают свои snap positions через scroll-snap-align: start.
scroll-snap-type: y mandatoryСкрольте по вертикали: контейнер ищет кандидатов по оси y и обязан завершить прокрутку на одном из них. Также обратите внимание, что из-за scroll-snap-align: start карточки будут снапиться к верхнему краю snapport к snap area. Удобно понять смысл, если вы с клавиатуры будете нажимать стрелки вверх и вниз.
<div class="gallery">
<article class="card">1</article>
<article class="card">2</article>
<article class="card">3</article>
<article class="card">4</article>
<article class="card">…</article>
</div>
.gallery {
display: flex;
flex-direction: column;
…
scroll-snap-type: y mandatory;
}
.card {
scroll-snap-align: start;
}
Тот же контейнер можно сделать мягче, если заменить mandatory на proximity.
scroll-snap-type: y proximityЗдесь ось та же самая, но притяжение мягче: если остановиться близко к карточке, браузер поможет дотянуться до неё, а если остановка далеко, может оставить естественную позицию прокрутки.
В итоге разница между mandatory и proximity такая:
mandatoryведёт себя строго и почти всегда доводит прокрутку до одной изsnap positions;proximityведёт себя мягче и снапит только когда пользователь уже остановился достаточно близко;- если нужна явная постраничность, обычно выбирают
mandatory, а если snapping должен быть лишь удобным дополнением, чаще подходитproximity.
Именно поэтому свойства в модуле разделены на две группы:
- контейнер описывает, по какой оси и с какой строгостью нужно искать snap-позиции;
- дочерний элемент описывает, какую область считать значимой и как именно её выравнивать.
Свойства для scroll-контейнера
Уф! Мы прошлись по основной терминологии и поняли, как работает Scroll Snap. Уже сейчас имеется полное понимание геометрии и работы snap-позиций. Теперь давайте быстро рассмотрим CSS-свойства и их синтаксис для использования в проекте.
1. scroll-snap-type
Это главное свойство, которое активирует механизм привязки для контейнера. Оно определяет, по каким осям работает snapping и насколько строго он применяется.
scroll-snap-type: none | [ x | y | block | inline | both ] [ mandatory | proximity ]?;
- Ось (axis):
x— горизонтальная ось;y— вертикальная;block— ось, соответствующая направлению блока (зависит от writing mode);inline— строчная ось;both— обе оси независимо.
- Строгость (strictness):
none— отключает привязку (значение по умолчанию);mandatory— браузер обязан остановиться на snap-позиции после завершения прокрутки;proximity— браузер может остановиться на snap-позиции, если пользователь остановился достаточно близко к ней (рекомендуется для мягкого взаимодействия).
Нюанс спецификации для всей страницы: если scroll-snap-type задан на root-элементе, он применяется к document viewport. При этом значение с body туда автоматически не переносится, поэтому для page-level snapping важно настраивать именно корневой элемент.
Здесь при прокрутке слайдер всегда остановится так, чтобы очередное изображение оказалось по центру контейнера.
2. scroll-padding
Свойство определяет внутренние отступы для snapport — области, которая считается «видимой» для расчёта привязки. Это полезно, когда часть контента перекрывается фиксированными элементами (шапка, подвал, тулбары).
scroll-padding: [ auto | <length-percentage> ]{1,4};
Значения задаются аналогично padding. auto позволяет браузеру самостоятельно определить отступ (например, под фиксированную панель).
Пример: контент с фиксированным тулбаром справа
<div class="container">
<div class="toolbar">Панель инструментов</div>
<div class="content">
<section>...</section>
<section>...</section>
<section>...</section>
</div>
</div>
.container {
position: relative;
height: 100vh;
overflow-y: auto;
scroll-snap-type: y mandatory;
scroll-padding: 0 200px 0 0; /* справа 200px под тулбар */
}
.toolbar {
position: fixed;
top: 0;
right: 0;
width: 200px;
height: 100%;
background: #eee;
}
.content section {
height: 100vh;
scroll-snap-align: start; /* привязать к началу каждой секции */
}
Благодаря scroll-padding snapport смещается влево на 200px, и секции выравниваются относительно оставшейся области, не заходя под тулбар.
Свойства для дочерних элементов (snap areas)
3. scroll-margin
Определяет внешние отступы для snap area, расширяя или сжимая область, участвующую в привязке. Отступы задаются как абсолютные длины и добавляются не просто к border box, а к области, которую спецификация строит из transformed border box элемента как axis-aligned bounding box.
scroll-margin: <length>{1,4};
Пример: добавим немного воздуха между элементами
Возьмём предыдущую галерею, но теперь хотим, чтобы при привязке между изображениями был небольшой зазор. Можно увеличить snap area:
.gallery img {
scroll-margin: 0 20px; /* добавить по 20px слева и справа */
}
В результате snap-позиция будет соответствовать не центру самого изображения, а центру расширенной области.
4. scroll-snap-align
Задаёт выравнивание snap area внутри snapport. Может принимать одно или два значения (для блочной и строчной осей соответственно).
scroll-snap-align: [ none | start | end | center ]{1,2};
none— элемент не создаёт snap-позиции.start— выравнивание по начальному краю (например, верх для вертикального скролла).end— по конечному краю.center— по центру.
Если указано одно значение, оно применяется к обеим осям.
5. scroll-snap-stop
Управляет тем, можно ли «перепрыгнуть» через snap-позицию при быстрой прокрутке. Полезно, когда нужно остановиться на каждом элементе, не пропуская их.
scroll-snap-stop: normal | always;
normal— разрешено проскальзывать мимо (по умолчанию).always— запрещено проходить мимо; прокрутка остановится на первой же snap-позиции.
Пример 1: обычная вертикальная лента (normal)
scroll-snap-stop: normalЗдесь разрешено проскальзывать мимо: при быстром скролле браузер может перескочить через некоторые snap-позиции и остановиться дальше.
.news-feed article {
scroll-snap-stop: normal;
}
Пример 2: пошаговая вертикальная прокрутка (always)
scroll-snap-stop: alwaysЗдесь каждая snap-позиция обязательна: даже при быстром движении контейнер старается остановиться на ближайшей секции и не пропускать шаги.
.strict-slides section {
scroll-snap-stop: always;
}
При таком подходе даже быстрое движение колесом мыши или свайп не позволит пропустить секцию — прокрутка остановится на каждой.
Особые случаи и рекомендации
Элементы больше области просмотра
Если snap area превышает размер snapport, браузер позволяет свободно прокручивать внутри такого элемента, пока он полностью не поместится. Принудительное выравнивание применяется только когда элемент снова становится меньше области просмотра.
Это важно для случаев, когда в секции много контента — пользователь может прокручивать её внутри, но при переходе к другой секции сработает привязка.
Попробуйте прокрутить галерею: на высоком блоке snapping временно не мешает дочитать содержимое внутри него.
<div class="story-gallery">
<section>...</section>
<section class="story-gallery__giant">Очень большой блок</section>
<section>...</section>
</div>
.story-gallery {
scroll-snap-type: y mandatory;
}
.story-gallery section {
scroll-snap-align: start;
}
Изменение контента (re-snapping)
Если содержимое динамически меняется (добавляются/удаляются элементы), браузер обязан пересчитать snap-позиции и, если контейнер был привязан к определённому элементу, остаться привязанным к нему (если он всё ещё существует). Это обеспечивает предсказуемое поведение в чатах, логах и лентах.
Поддержка браузерами
Scroll Snap добавлен в инициативу Interop 2026. Это для нас значит, что до конца года поддержка всеми браузерами будет практически идеальной.
CSS Scroll Snap Level 1 поддерживается во всех современных браузерах (Chrome, Firefox, Safari, Edge) начиная с 2018–2019 годов. Для старых версий могут потребоваться префиксы, но сейчас они не обязательны. Рекомендуется всегда проверять актуальную информацию на caniuse.com.
Что нового в CSS Scroll Snap Module Level 2
У CSS Scroll Snap Module Level 2 другой фокус: он почти не меняет базовую геометрию snapping, зато добавляет возможности для начальной позиции прокрутки, стилизации текущего snapped-элемента и событий, через которые можно реагировать на смену snap-target.
Но здесь важно сделать честную оговорку. По состоянию на 22 марта 2026 года CSS Scroll Snap Module Level 2 остаётся черновиком, и полной межбраузерной реализации у него нет. Более того, опубликованный W3C TR использует имя scroll-start-target, а в актуальной Chromium-реализации фигурирует уже другое имя — scroll-initial-target. Поэтому этот раздел лучше воспринимать как обзор направления развития Scroll Snap, а не как production-ready API, который одинаково работает везде.
1. Начальная позиция: scroll-start-target
Если в Level 1 мы управляли тем, как контейнер снапится во время прокрутки, то Level 2 добавляет ещё один вопрос: с какого элемента контейнер должен быть показан уже при первом появлении на странице.
Именно для этого в опубликованном TR и вводится scroll-start-target.
scroll-start-target: none | auto;
none— элемент не участвует в выборе начальной позиции;auto— элемент становится кандидатом на роль initial scroll target для ближайшего scroll container.
Идея такая:
- контейнер ещё не прокручивали вручную;
- внутри него есть один или несколько дочерних элементов с
scroll-start-target: auto; - браузер выбирает initial scroll target и вычисляет начальную scroll position ещё на первом layout.
Если таких элементов несколько, спецификация рекомендует взять первый в tree order. Если ни одного нет, начальная позиция определяется как обычно, без дополнительной помощи от Scroll Snap Level 2.
Отдельно важно, как именно считается эта стартовая позиция. Спецификация связывает её с алгоритмом scroll-into-view: по сути браузер должен вычислить позицию так, как будто он скроллит контейнер к нужному элементу с behavior: auto, block: start и inline: nearest.
Ещё два нюанса из Level 2:
- если начальная позиция потенциально могла быть задана и через
place-content, и черезscroll-start-target, побеждает именноscroll-start-target; - если нужный элемент появился не в самый первый момент, а чуть позже, уже после первого layout, браузер всё равно может и должен довести контейнер до него, если нет причин считать, что пользователю это уже не нужно.
Пример из идеи Level 2:
<div class="carousel">
<img src="img1.jpg" alt="" />
<img src="img2.jpg" alt="" />
<img class="origin" src="img3.jpg" alt="" />
<img src="img4.jpg" alt="" />
</div>
.carousel {
scroll-snap-type: x mandatory;
}
.carousel img {
scroll-snap-align: center;
}
.carousel .origin {
scroll-start-target: auto;
}
В такой модели карусель может открываться сразу на третьем изображении, не требуя отдельного scrollTo() из JavaScript.
Но ещё раз: именно имя scroll-start-target из опубликованного TR сейчас не стоит считать кроссбраузерно доступным API. В Chromium развивается близкая по смыслу реализация, но уже под другим названием.
2. Стилизация текущего snap-target: :snapped
Следующая идея Level 2 — дать CSS способ понять, какой именно элемент сейчас выбран как snap-target, и стилизовать его без JavaScript.
Для этого draft описывает семейство псевдоклассов:
:snapped:snapped-x:snapped-y:snapped-inline:snapped-block
Смысл у них простой:
:snapped— матчится на текущем snapped-элементе независимо от оси;- осевые варианты позволяют точнее указать, по какой именно оси элемент сейчас выбран браузером.
На бумаге это очень удобно. Например, можно было бы подсвечивать текущую карточку в карусели чистым CSS:
.carousel__slide:snapped {
outline: 3px solid dodgerblue;
}
Но здесь есть важная оговорка прямо из самого Level 2: раздел с :snapped планируют убрать в пользу подхода через container state query. То есть это не та часть спецификации, на которую стоит опираться как на устоявшуюся и финальную модель.
Практически это означает следующее:
- как идея для будущего — раздел полезен;
- как production-инструмент — нет, пока рано.
3. Snap-события: scrollsnapchange и scrollsnapchanging
Самая прикладная часть Level 2 — это snap events. Они нужны для случаев, когда вам мало просто “знать, что контейнер снапится”, и хочется понять:
- к какому элементу контейнер уже привязался;
- к какому элементу он, скорее всего, привяжется через мгновение;
- изменилась ли snapped-позиция из-за layout changes, а не только из-за жеста пользователя.
Спецификация вводит два события:
scrollsnapchange— приходит, когда контейнер уже перешёл к новому snap-target;scrollsnapchanging— приходит во время прокрутки, когда браузер уже понимает, какой target станет следующим.
У обоих событий тип SnapEvent, и в нём есть два полезных поля:
snapTargetBlocksnapTargetInline
Через них можно понять, какой элемент выбран по каждой оси.
Пример:
scroller.addEventListener('scrollsnapchange', (event) => {
console.log('block:', event.snapTargetBlock);
console.log('inline:', event.snapTargetInline);
});
scroller.addEventListener('scrollsnapchanging', (event) => {
console.log('next block:', event.snapTargetBlock);
console.log('next inline:', event.snapTargetInline);
});
Это удобно, например, для:
- индикаторов активного слайда;
- синхронизации пагинации и прокрутки;
- подгрузки данных под будущий snap-target;
- анимаций, которые должны стартовать строго в момент выбора новой секции.
Есть и важные детали из спецификации:
scrollsnapchangeдолжен приходить в конце прокрутки, но доscrollend;scrollsnapchangingприходит во время прокрутки, когда потенциальный target уже изменился;- события могут возникать не только из-за жеста пользователя, но и из-за layout changes, если контейнер после re-snapping оказался привязан уже к другому элементу.