Als je programmatisch naar een gedeelte of element scrollt (vooral met behavior: 'smooth'), wordt een vervolgactie onmiddellijk geactiveerd, zoals een markeringsstatus laten knipperen, een invoeranimatie starten of de focus verplaatsen. Hierdoor wordt het effect uitgevoerd terwijl het doel nog buiten het scherm is of halverwege het scrollen.
Eerder moesten ontwikkelaars de scrollduur raden met setTimeout() (die te vroeg wordt geactiveerd bij lang scrollen en te laat bij kort scrollen) of handmatig een scrollend-gebeurtenislistener verpakken in een Promise met een reservetimer (omdat scrollend niet wordt geactiveerd als het doel al in beeld is en er niet wordt gescrold).
De programmatische scrollmethoden op Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) en Window (scroll(), scrollTo(), scrollBy()) retourneren een Promise die wordt omgezet als de scrollbewerking is afgerond. Als het doel al op de gevraagde scrollpositie staat en scrollen niet nodig is, wordt de geretourneerde Promise onmiddellijk opgelost zonder te blijven hangen.
Implementatie
- Markeer de handler als
async: Gebruik eenasync-functie voor de gebeurtenislistener of navigatiehandler die het scrollen activeert. - Wacht rechtstreeks op de scrollmethode: Roep
await element.scrollIntoView({ behavior: 'smooth', ... })aan (ofawait element.scrollTo(...),await window.scrollTo(...), enzovoort). De uitvoering wordt onderbroken totdat de browser de scrollanimatie heeft afgerond en de scrollpositie is vastgesteld. - Het effect na scrollen toepassen: Schakel de markeringsklasse in, start de animatie of verplaats de focus nadat de expressie
awaitis voltooid.
Onderbroken scrollen en focusbeheer
- Snel/onderbroken scrollen verwerken: Als een nieuwe scrollactie wordt gestart in dezelfde scrollcontainer voordat een doorlopende vloeiende scrollactie is voltooid, breekt de browser de doorlopende vloeiende scrollactie af en lost deze alle openstaande scrollbeloften op (wijst deze niet af) voor die container. Als je het effect na het scrollen wilt onderdrukken voor vervangen scrolls wanneer een gebruiker snel klikt, houd je een oplopende bewerkings-ID bij en keer je vroeg terug na
awaitals er een nieuwere scroll is gestart:
let currentScrollId = 0;
const sections = document.querySelectorAll('section');
async function scrollToAndHighlight(targetElement) {
const scrollId = ++currentScrollId;
sections.forEach((sec) => sec.classList.remove('highlight'));
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Abort follow-up effect if another scroll was triggered while awaiting
if (scrollId !== currentScrollId) return;
targetElement.classList.add('highlight');
}
- De focus op het doel richten na scrollen: Als je de toetsenbordfocus na
await targetElement.scrollIntoView(...)naar het doelelement (of een kop daarin mettabindex="-1") verplaatst, geef je{ preventScroll: true }door aanfocus()zodat de focusaanroep geen tweede, concurrerende scroll start:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });
Beperkingen en best practices
- VERPLICHT: Wacht op de
Promisedie rechtstreeks wordt geretourneerd doorscrollIntoView(),scroll(),scrollTo()ofscrollBy()in plaats van de scrollduur te raden metsetTimeout(). - Verpak
scrollendNIET in een handmatigenew Promise()als primaire implementatie.scrollendwordt niet geactiveerd als het doel al in beeld is en er geen scrollafstand is afgelegd, terwijl scrollmethode-promises in dat geval automatisch worden omgezet. - WEL DOEN: Geef
{ preventScroll: true }door als u.focus()aanroept nadat u op een scrollmethode heeft gewacht, zodat de browser niet een 2e keer springt of scrollt. - WEL DOEN: Beveilig effecten na het scrollen met een verzoektoken als snelle gebruikersinteracties overlappende vloeiende scrolls in dezelfde container kunnen activeren, omdat het onderbreken van een vloeiende scroll de vorige promise oplost (in plaats van afwijst).
- Bekende Chrome-bug met de rootscroller: Chrome heeft een bug waarbij de
Promisedie wordt geretourneerd doorscrollIntoView()te vroeg wordt opgelost als de rootscroller van het document scrollt. Wachten op eenscrollIntoView()in een geneste scrollcontainer werkt zoals verwacht. Verpak je content niet in een scrollcontainer, zoalsdivmetoverflow-y: auto, als tijdelijke oplossing voor deze bug.
Reservestrategieën
In browsers die geen scrollmethode-promises ondersteunen, retourneren scrollIntoView(), scroll(), scrollTo() en scrollBy() undefined in plaats van een Promise.
- Progressive enhancement (standaardgedrag van
await): Omdatawait undefinedgeldige JavaScript is die onmiddellijk wordt omgezet in de volgende microtaak, leidt het schrijven vanawait element.scrollIntoView({ behavior: 'smooth' })niet tot een fout in niet-ondersteunde browsers. In plaats daarvan wordt de code na het scrollen uitgevoerd zodra het scrollen begint. Voor niet-essentiële visuele versieringen, zoals een tijdelijke markering, is dit vaak voldoende. - Trouwe fallback (
scrollend+ time-out): Als je Baseline-doelscroll-promisesniet ondersteunt en de actie na het scrollen moet wachten tot het scrollen is voltooid in oudere browsers, controleer je of de retourwaarde eenPromise(result instanceof Promise) is. Als ditundefinedis, wacht je op de gebeurtenisscrollendin de scrollcontainer met een korte fallback-time-out voor het geval het element al in beeld is en er niet wordt gescrold:
function scrollIntoViewAsync(element, options, scroller = window) {
const result = element.scrollIntoView(options);
// Feature detect: in supporting browsers, scrollIntoView() returns a Promise
if (result instanceof Promise) {
return result;
}
// Fallback for browsers where scrollIntoView() returns undefined
return new Promise((resolve) => {
let timeoutId;
const finish = () => {
clearTimeout(timeoutId);
scroller.removeEventListener('scrollend', finish);
scroller.removeEventListener('scroll', onScroll);
resolve();
};
const onScroll = () => {
// Restart the idle timer on every scroll event. If scrollend never fires
// (browsers without scrollend support), this resolves once scrolling goes idle.
clearTimeout(timeoutId);
timeoutId = setTimeout(finish, 100);
};
scroller.addEventListener('scrollend', finish, { once: true });
scroller.addEventListener('scroll', onScroll, { passive: true });
// If no scroll event fires within the idle window (e.g., already in view), resolve.
// 100ms is an example value; it only needs to exceed the gap between scroll events.
timeoutId = setTimeout(finish, 100);
});
}