Sincronização do estado de ajuste de rolagem

A sincronização do estado da interface com a posição de ajuste de um contêiner rolável tradicionalmente exigia listeners de eventos de rolagem complexos, cálculos manuais de deslocamentos de rolagem e observadores de interseção. O evento scrollsnapchange oferece uma maneira nativa e eficiente de detectar quando um scroller se estabeleceu em um novo destino de ajuste, o que é útil para sincronizar barras laterais ou destacar a seção ativa em um sumário.

Implementação

1. Configurar o ajuste de rolagem no CSS

O contêiner precisa ter scroll-snap-type definido e ter filhos com scroll-snap-align para que o navegador rastreie os destinos de ajuste. Em um artigo longo com um sumário, você pode usar isso para fixar os cabeçalhos das seções na parte de cima da janela de visualização.

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. Detectar mudanças de ajuste

Use o evento scrollsnapchange no contêiner de rolagem para reagir quando o usuário terminar de rolar e o navegador se ajustar a um novo elemento. Na nossa demonstração de sumário, usamos isso para destacar o link ativo na barra lateral.

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

Acessibilidade

Para um sumário, verifique se os links da barra lateral usam aria-current="true" ou aria-current="location" quando estão ativos.

Além disso, tenha cuidado ao usar o valor mandatory para scroll-snap-type, porque isso pode fazer com que o conteúdo entre os pontos de ajuste fique inacessível quando for maior que a tela.

Estratégias de fallback

Se scrollsnapchange não for compatível, use IntersectionObserver para detectar qual elemento está na parte de cima do scroller. Esse comportamento é diferente de scrollsnapchange, porque ele é acionado durante a rolagem, e não apenas quando ela é concluída.

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