Synchronisation de l'état du défilement scroll-snap

La synchronisation de l'état de l'UI avec la position d'alignement d'un conteneur à défilement nécessitait traditionnellement des écouteurs d'événements de défilement complexes, des calculs manuels des décalages de défilement et des observateurs d'intersection. L'événement scrollsnapchange offre un moyen natif et efficace de détecter quand un défileur s'est arrêté sur une nouvelle cible d'alignement. Il est donc utile pour synchroniser les barres latérales ou mettre en surbrillance la section active d'une table des matières.

Implémentation

1. Configurer le défilement scroll-snap en CSS

Le conteneur doit avoir scroll-snap-type défini et avoir des enfants avec scroll-snap-align pour que le navigateur puisse suivre les cibles d'alignement. Dans un long article avec une table des matières, vous pouvez l'utiliser pour ancrer les en-têtes de section en haut de la fenêtre d'affichage.

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. Écouter les modifications d'alignement

Utilisez l'événement scrollsnapchange sur le conteneur de défilement pour réagir lorsque l'utilisateur a terminé de faire défiler la page et que le navigateur s'aligne sur un nouvel élément. Dans notre démonstration de table des matières, nous l'utilisons pour mettre en évidence le lien actif dans la barre latérale.

<!-- 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);
  }
});

Accessibilité

Pour une table des matières, assurez-vous que les liens de la barre latérale utilisent aria-current="true" ou aria-current="location" lorsqu'ils sont actifs.

En outre, soyez prudent lorsque vous utilisez la valeur mandatory pour scroll-snap-type, car cela peut rendre inaccessible le contenu entre les points d'ancrage lorsqu'il est plus long que l'écran.

Stratégies de remplacement

Si scrollsnapchange n'est pas compatible, utilisez IntersectionObserver pour détecter l'élément qui se trouve actuellement en haut du défilement. Notez que ce comportement est différent de celui de scrollsnapchange, car il se déclenche pendant le défilement, et non seulement lorsque le défilement est terminé.

// 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));
}