Overzicht
Gebruikers verwachten onmiddellijke visuele feedback bij interactie met UI-elementen zoals carrousels of galerijen. Traditionele scrolluitlijning geeft alleen feedback nadat het scrollgebaar is afgerond en het element is geplaatst. Door Scroll Snap Events te gebruiken, met name scrollsnapchanging, kun je realtime feedback geven tijdens het scrollgebaar en het in behandeling zijnde uitlijningsdoel markeren voordat de gebruiker de aanraking of muis loslaat.
Implementatie
1. Luister naar scrollsnapchanging
Voeg een event-listener voor scrollsnapchanging toe aan de scrollcontainer. Deze gebeurtenis wordt geactiveerd als de browser bepaalt dat er waarschijnlijk een nieuw uitlijndoel wordt geselecteerd.
const container = document.querySelector('#gallery');
const thumbnails = document.querySelectorAll('.thumbnail');
const items = document.querySelectorAll('.gallery-item');
container.addEventListener('scrollsnapchanging', (event) => {
// Highlight pending snap target during scroll for real-time feedback.
const pendingTarget = event.snapTargetInline;
const index = [...items].indexOf(pendingTarget);
if (index === -1 || !thumbnails[index]) return;
// Use lightweight class toggle to avoid layout thrashing during rapid events.
// Note: aria-current is NOT toggled here. It tracks the settled "current"
// item, which is updated in the scrollsnapchange handler below.
thumbnails.forEach((thumb) => thumb.classList.remove('pending'));
thumbnails[index].classList.add('pending');
});
In dit voorbeeld wordt snapTargetInline gebruikt omdat de galerij horizontaal scrollt. Als je scrollcontainer verticaal scrollt, gebruik je snapTargetBlock.
2. Luister naar scrollsnapchange
Als je de status wilt afronden wanneer het scrollgebaar is voltooid en het element daadwerkelijk vastklikt, luister je naar de gebeurtenis scrollsnapchange. Dit is vereist om de uiteindelijke actieve status vast te stellen.
container.addEventListener('scrollsnapchange', (event) => {
// Promote pending state to active on scroll completion.
const snappedTarget = event.snapTargetInline;
const index = [...items].indexOf(snappedTarget);
if (index === -1 || !thumbnails[index]) return;
// Establish final active state and clean up pending.
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[index].classList.add('active');
thumbnails[index].setAttribute('aria-current', 'true');
});
3. Synchronisatie van de beginstatus
Als de pagina wordt geladen, kan de scrollpositie worden hersteld door de browser (bijvoorbeeld via geschiedenisdoorloop of een ankerlink). scrollsnapchange- en scroll-gebeurtenissen worden niet automatisch geactiveerd. Voer een eenmalige geometrische controle uit om de UI te synchroniseren met de oorspronkelijke scrollpositie.
// Note: For item.offsetLeft to be relative to the container,
// the container MUST be the offsetParent (e.g., `position: relative`).
const findClosestItemIndex = () => {
// Center-distance assumes scroll-snap-align: center on items.
// For start-aligned snap, compare scrollLeft to item.offsetLeft directly.
const containerCenter = container.scrollLeft + container.clientWidth / 2;
let closestIndex = 0;
let minDistance = Infinity;
items.forEach((item, index) => {
const itemCenter = item.offsetLeft + item.offsetWidth / 2;
const distance = Math.abs(containerCenter - itemCenter);
if (distance < minDistance) {
minDistance = distance;
closestIndex = index;
}
});
return closestIndex;
};
const initActiveItem = () => {
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[closestIndex].classList.add('active');
thumbnails[closestIndex].setAttribute('aria-current', 'true');
};
if (document.readyState === 'complete') {
initActiveItem();
} else {
window.addEventListener('load', initActiveItem, { once: true });
}
Reservestrategieën
Voor browsers die scrollsnapchanging niet ondersteunen, biedt de UI standaard geen snelle feedback tijdens het scrollgebaar en wordt de gekoppelde UI niet meer gesynchroniseerd met de content.
VERPLICHT: Zorg voor een fallback voor browsers zonder ondersteuning, anders wordt de gekoppelde UI niet meer gesynchroniseerd met de content.
Simuleer de realtime aard van scrollsnapchanging met een scroll-event-listener gekoppeld aan requestAnimationFrame en geometrische afstandsberekeningen om het dichtstbijzijnde uitlijndoel te bepalen terwijl de gebruiker actief scrollt.
if ('onscrollsnapchanging' in Element.prototype) {
// Use native scroll snap events
} else {
// Fallback: use scroll + requestAnimationFrame for eager feedback
// (assumes the same container, thumbnails, items defined in step 1)
let scrollTimeout;
let rafId = null;
const promotePendingToActive = () => {
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
thumbnails.forEach((thumb) => {
thumb.classList.remove('pending', 'active');
thumb.removeAttribute('aria-current');
});
thumbnails[closestIndex].classList.add('active');
thumbnails[closestIndex].setAttribute('aria-current', 'true');
};
container.addEventListener('scroll', () => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = null;
const closestIndex = findClosestItemIndex();
if (!thumbnails[closestIndex]) return;
// DO NOT forget to clean up stale pending classes
thumbnails.forEach((thumb) => thumb.classList.remove('pending'));
thumbnails[closestIndex].classList.add('pending');
});
// Debounce fallback for browsers that don't support scrollend
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(promotePendingToActive, 100);
}, { passive: true });
// Fallback: use Baseline `scrollend` event to promote pending to active cleanly where supported
container.addEventListener('scrollend', () => {
clearTimeout(scrollTimeout);
promotePendingToActive();
});
}
De geometrische scroll + requestAnimationFrame-reserve simuleert het gedrag van native voorspelling van vastzetten nauwkeurig, waaronder de juiste verwerking van programmatisch scrollen. Omdat functies zoals scrollIntoView op natuurlijke wijze scroll-gebeurtenissen activeren tijdens de uitvoering, blijft de UI soepel gesynchroniseerd tijdens de scrollanimatie zonder dat er extra aangepaste logica nodig is.