Раньше для синхронизации состояния пользовательского интерфейса с позицией привязки прокручиваемого контейнера требовались сложные прослушиватели событий прокрутки, вычисление смещений прокрутки вручную и наблюдатели пересечений. Событие scrollsnapchange позволяет эффективно и без сторонних инструментов определять, когда прокрутка останавливается на новой цели привязки. Это полезно для синхронизации боковых панелей или выделения активного раздела в оглавлении.
Реализация
1. Как настроить привязку прокрутки в CSS
Чтобы браузер мог отслеживать цели привязки, у контейнера должно быть определено свойство scroll-snap-type, а у дочерних элементов – свойство scroll-snap-align. В длинной статье с оглавлением вы можете использовать это, чтобы привязать заголовки разделов к верхней части области просмотра.
main {
/* Enable scroll snapping on the container */
scroll-snap-type: y proximity;
overflow-y: auto;
}
h2 {
/* Define how headers align when snapped */
scroll-snap-align: start;
}
2. Прослушивание изменений привязки
Используйте событие scrollsnapchange в контейнере прокрутки, чтобы реагировать, когда пользователь завершает прокрутку и браузер привязывается к новому элементу. В нашей демонстрации оглавления мы используем это, чтобы выделить активную ссылку на боковой панели.
<!-- MANDATORY: Wrap table of contents links inside a proper navigation landmark -->
<nav aria-label="Table of contents">
<ul>
<li><a href="#section-1" aria-current="location">Section 1</a></li>
<li><a href="#section-2">Section 2</a></li>
</ul>
</nav>
const main = document.getElementById('main');
const links = document.querySelectorAll('nav a');
// The event fires when the scroller settles on a new snap target
main.addEventListener('scrollsnapchange', (event) => {
// Use snapTargetBlock for vertical or snapTargetInline for horizontal
const snappedHeader = event.snapTargetBlock;
if (snappedHeader) {
setSelectedParagraph(snappedHeader.id);
}
});
Специальные возможности
Для оглавления убедитесь, что ссылки на боковой панели используют aria-current="true" или aria-current="location", когда они активны.
Кроме того, будьте осторожны при использовании значения mandatory для scroll-snap-type, так как это может привести к тому, что контент между точками привязки станет недоступным, если он длиннее экрана.
Резервные стратегии
Если scrollsnapchange не поддерживается, используйте IntersectionObserver, чтобы определить, какой элемент сейчас находится вверху прокручиваемой области. Обратите внимание, что это отличается от поведения scrollsnapchange, поскольку триггер будет срабатывать во время прокрутки, а не только когда она завершится.
// Feature detect support for scroll snap events
if (!('onscrollsnapchange' in HTMLElement.prototype)) {
const observer = new IntersectionObserver(
() => {
// Each time the set of intersecting headers changes, find the top
// header that is visible.
const topEntry = [...headers].reduce((currentTop, header) => {
// Use the bottom to handle scrolling up, when the top is still offscreen
const {bottom} = header.getBoundingClientRect();
// Don't match if the header's bottom is above the scrollport
if (bottom < 0) return;
if (!currentTop) return header;
return bottom <
currentTop.getBoundingClientRect().bottom
? header
: currentTop;
}, undefined);
if (topEntry) setSelectedParagraph(topEntry.id);
},
{ root: main, threshold: 0.9 // Adjust based on your use case },
);
// Observe all snap targets (e.g., section headers)
document.querySelectorAll('h2').forEach(header => observer.observe(header));
}