Effecten activeren nadat programmatisch scrollen is voltooid

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

  1. Markeer de handler als async: Gebruik een async-functie voor de gebeurtenislistener of navigatiehandler die het scrollen activeert.
  2. Wacht rechtstreeks op de scrollmethode: Roep await element.scrollIntoView({ behavior: 'smooth', ... }) aan (of await element.scrollTo(...), await window.scrollTo(...), enzovoort). De uitvoering wordt onderbroken totdat de browser de scrollanimatie heeft afgerond en de scrollpositie is vastgesteld.
  3. Het effect na scrollen toepassen: Schakel de markeringsklasse in, start de animatie of verplaats de focus nadat de expressie await is 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 await als 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 met tabindex="-1") verplaatst, geef je { preventScroll: true } door aan focus() 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 Promise die rechtstreeks wordt geretourneerd door scrollIntoView(), scroll(), scrollTo() of scrollBy() in plaats van de scrollduur te raden met setTimeout().
  • Verpak scrollend NIET in een handmatige new Promise() als primaire implementatie. scrollend wordt 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 Promise die wordt geretourneerd door scrollIntoView() te vroeg wordt opgelost als de rootscroller van het document scrollt. Wachten op een scrollIntoView() in een geneste scrollcontainer werkt zoals verwacht. Verpak je content niet in een scrollcontainer, zoals div met overflow-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): Omdat await undefined geldige JavaScript is die onmiddellijk wordt omgezet in de volgende microtaak, leidt het schrijven van await 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-doel scroll-promises niet ondersteunt en de actie na het scrollen moet wachten tot het scrollen is voltooid in oudere browsers, controleer je of de retourwaarde een Promise (result instanceof Promise) is. Als dit undefined is, wacht je op de gebeurtenis scrollend in 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);
  });
}