Effekte nach Abschluss des programmatischen Scrollens auslösen

Wenn Sie programmatisch zu einem Abschnitt oder Element scrollen (insbesondere mit behavior: 'smooth') und sofort eine Folgeaktion auslösen, z. B. einen Hervorhebungsstatus aufblinken lassen, eine Einstiegsanimation starten oder den Fokus verschieben, wird der Effekt ausgeführt, während sich das Ziel noch außerhalb des Bildschirms oder mitten im Scrollvorgang befindet.

Bisher mussten Entwickler die Scroll-Dauer mit setTimeout() schätzen (was bei langem Scrollen zu früh und bei kurzem Scrollen zu spät ausgelöst wird) oder einen scrollend-Event-Listener manuell in einen Promise mit einem Fallback-Timer einbinden (da scrollend nicht ausgelöst wird, wenn das Ziel bereits sichtbar ist und kein Scrollen stattfindet).

Die programmatischen Scrollmethoden für Element (scrollIntoView(), scroll(), scrollTo(), scrollBy()) und Window (scroll(), scrollTo(), scrollBy()) geben ein Promise zurück, das aufgelöst wird, wenn der Scrollvorgang abgeschlossen ist. Wenn sich das Ziel bereits an der angeforderten Scrollposition befindet und kein Scrollen erforderlich ist, wird das zurückgegebene Promise sofort aufgelöst, ohne dass es zu Verzögerungen kommt.

Implementierung

  1. Handler als async markieren: Verwenden Sie eine async-Funktion für den Event-Listener oder Navigations-Handler, der das Scrollen auslöst.
  2. Warten Sie direkt auf die Scrollmethode: Rufen Sie await element.scrollIntoView({ behavior: 'smooth', ... }) (oder await element.scrollTo(...), await window.scrollTo(...) usw.) auf. Die Ausführung wird pausiert, bis der Browser die Scrollanimation beendet hat und die Scrollposition feststeht.
  3. Post-Scroll-Effekt anwenden: Schalten Sie die Hervorhebungsklasse um, starten Sie die Animation oder verschieben Sie den Fokus, nachdem der Ausdruck await abgeschlossen ist.

Unterbrochenes Scrollen und Fokusverwaltung

  • Umgang mit schnellem/unterbrochenem Scrollen: Wenn ein neuer Scrollvorgang im selben Scrollcontainer gestartet wird, bevor ein laufender optimierter Scrollvorgang abgeschlossen ist, bricht der Browser den laufenden optimierten Scrollvorgang ab und löst alle ausstehenden Scroll-Promises für diesen Container auf (lehnt sie nicht ab). Wenn Sie den Effekt nach dem Scrollen für ersetzte Scrollvorgänge unterdrücken möchten, wenn ein Nutzer schnell klickt, können Sie eine inkrementierende Vorgangs-ID erfassen und nach await vorzeitig zurückkehren, wenn ein neuer Scrollvorgang gestartet wurde:
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');
}
  • Fokus nach dem Scrollen auf das Ziel legen: Wenn Sie den Tastaturfokus nach await targetElement.scrollIntoView(...) auf das Zielelement (oder eine Überschrift darin mit tabindex="-1") verschieben, übergeben Sie { preventScroll: true } an focus(), damit der Fokusaufruf keinen zweiten, konkurrierenden Scrollvorgang auslöst:
await targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetHeading.focus({ preventScroll: true });

Einschränkungen und Best Practices

  • OBLIGATORISCH: Warten Sie auf das Promise, das direkt von scrollIntoView(), scroll(), scrollTo() oder scrollBy() zurückgegeben wird, anstatt die Scroll-Dauer mit setTimeout() zu schätzen.
  • Verpacken Sie scrollend NICHT in einem manuellen new Promise() als primäre Implementierung. scrollend wird nicht ausgelöst, wenn das Ziel bereits sichtbar ist und keine Scroll-Distanz zurückgelegt wird. Scroll-Methoden-Promises werden in diesem Fall automatisch aufgelöst.
  • EMPFOHLEN: Übergeben Sie { preventScroll: true }, wenn Sie .focus() aufrufen, nachdem Sie auf eine Scrollmethode gewartet haben, damit der Browser nicht ein zweites Mal springt oder scrollt.
  • EMPFOHLEN: Schützen Sie Effekte nach dem Scrollen mit einem Anfrage-Token, wenn schnelle Nutzerinteraktionen überlappende optimierte Scrollvorgänge im selben Container auslösen können. Das Unterbrechen eines optimierten Scrollvorgangs löst das vorherige Promise auf, anstatt es abzulehnen.
  • Bekannter Chrome-Fehler mit dem Root-Scroller: In Chrome gibt es einen Fehler, bei dem das von scrollIntoView() zurückgegebene Promise zu früh aufgelöst wird, wenn der Root-Scroller des Dokuments scrollt. Das Warten auf ein scrollIntoView() in einem verschachtelten Scroll-Container funktioniert wie erwartet. Verpacken Sie Ihre Inhalte nicht in einen Scroll-Container, z. B. div mit overflow-y: auto, um diesen Fehler zu umgehen.

Fallback-Strategien

In Browsern, die keine Promise-Objekte für Scrollmethoden unterstützen, geben scrollIntoView(), scroll(), scrollTo() und scrollBy() undefined anstelle eines Promise zurück.

  • Progressive Verbesserung (Standardverhalten von await): Da await undefined gültiges JavaScript ist, das sofort im nächsten Mikrotask aufgelöst wird, wird beim Schreiben von await element.scrollIntoView({ behavior: 'smooth' }) in nicht unterstützten Browsern kein Fehler ausgegeben. Stattdessen wird der Code für die Zeit nach dem Scrollen ausgeführt, sobald das Scrollen beginnt. Für unkritische visuelle Elemente wie eine temporäre Hervorhebung ist das oft ausreichend.
  • Zuverlässiger Fallback (scrollend + Zeitlimit): Wenn Ihr Baseline-Ziel scroll-promises nicht unterstützt und die Aktion nach dem Scrollen in älteren Browsern warten muss, bis das Scrollen abgeschlossen ist, prüfen Sie, ob der Rückgabewert ein Promise (result instanceof Promise) ist. Wenn es undefined ist, warten Sie auf das scrollend-Ereignis im Scrollcontainer mit einem kurzen Fallback-Zeitlimit, falls das Element bereits sichtbar ist und kein Scrollen erfolgt:
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);
  });
}