Criar uma navegação Scrollspy

Uma navegação scrollspy é um padrão de interface comum que destaca automaticamente o link de navegação correspondente à seção da página que está na janela de visualização. Isso dá aos usuários uma noção clara de onde estão em um documento longo enquanto rolam a tela.

Esse padrão é comum em dois cenários:

  • Sites de página única: em que a navegação principal do site leva a diferentes seções de uma única página, em vez de URLs separados.
  • Conteúdo longo: quando um sumário ou menu lateral de navegação acompanha o progresso do usuário em um artigo, guia ou documentação.

Como fazer a implementação

Tradicionalmente, o padrão scrollspy exigia JavaScript e a API IntersectionObserver. O CSS moderno oferece uma maneira nativa de fazer isso usando a propriedade scroll-target-group e a pseudoclasse :target-current.

  1. Verifique se os links de navegação usam identificadores de fragmento: sua navegação precisa consistir em links fixos (<a>) que apontam para seções na mesma página usando identificadores de fragmento (por exemplo, <a href="#section-1">) que correspondem ao id das suas seções de conteúdo.
  2. Ativar o agrupamento de destinos de rolagem: aplique a propriedade scroll-target-group: auto ao contêiner dos seus links de navegação. Isso informa ao navegador que esses links devem ser tratados como marcadores que rastreiam os respectivos destinos.
  3. Estilizar o link ativo: use a pseudoclasse :target-current nos links de navegação para aplicar estilos quando a seção de destino estiver visível no contêiner de rolagem.
  4. Estado semântico: use um script para atualizar aria-current="true" no link ativo quando a rolagem parar para garantir que o estado seja comunicado corretamente às tecnologias adaptativas.

Exemplo de código

Este exemplo destaca o link ativo em um menu de navegação fixo conforme o usuário rola pelas seções.


/* MANDATORY: The container of markers must have scroll-target-group: auto */
nav {
  /* Enables automatic tracking of fragment links */
  scroll-target-group: auto;
}
/* MANDATORY: Use :target-current to style the link whose target is in view. */
nav a:target-current {
  color: var(--accent, #007bff);
  border-bottom-color: var(--accent, #007bff);
}
<nav>
  <a href="#intro">Introduction</a>
  <a href="#features">Features</a>
  <a href="#pricing">Pricing</a>
</nav>

<main>
  <!-- MANDATORY: Each section must have a unique ID matching a link fragment in the <nav>. -->
  <section id="intro">
    <h2>Introduction</h2>
    <!-- content -->
  </section>
  <section id="features">
    <h2>Features</h2>
    <!-- content -->
  </section>
  <section id="pricing">
    <h2>Pricing</h2>
    <!-- content -->
  </section>
</main>

<script type="module">
  if (CSS.supports('scroll-target-group: auto')) {
    const syncAriaCurrent = () => {
      const currentLink = document.querySelector('nav a:target-current');
      document.querySelectorAll('nav a').forEach(link => {
        link.setAttribute('aria-current', link === currentLink ? 'true' : 'false');
      });
    };
    // Update the aria on initial page load. Ensure the DOM content is loaded, either by using `<script type="module">` or an event listener for `DOMContentLoaded`.
    syncAriaCurrent();
    // Update the aria after scroll ends
    document.addEventListener('scrollend', syncAriaCurrent);
  }
</script>

Práticas recomendadas

  • USE scroll-target-group: auto em um contêiner dedicado para links de navegação: isso garante que o navegador saiba quais links devem ser rastreados.
  • É RECOMENDÁVEL garantir que os destinos tenham atributos id exclusivos que correspondam aos atributos href dos links.
  • FORNEÇA espaço vertical suficiente para as seções: o scroll-target-group identifica com mais clareza o destino visível quando as seções são grandes o suficiente para que apenas uma ou duas fiquem visíveis por vez.
  • OBRIGATÓRIO: para acessibilidade, o estado visual precisa ser espelhado com aria-current. Embora o :target-current lide com o aspecto visual, ele não é exposto aos leitores de tela.
  • AVOID usar apenas cores para o estado ativo: inclua outras dicas visuais, como espessura da fonte, sublinhado ou um ponto indicador, ou garanta que haja contraste suficiente entre as cores para que a diferença seja acessível a usuários com deficiências de visão de cores.

Estratégias de fallback

Para navegadores que não são compatíveis com scroll-target-group, use um substituto com base na API IntersectionObserver.

<script>
  // Feature detection for scroll-target-group
  const supportsScrollTargetGroup = CSS.supports('scroll-target-group: auto');

  if (!supportsScrollTargetGroup) {
    const navLinks = document.querySelectorAll('nav a');
    const sections = document.querySelectorAll('main section');

    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const id = entry.target.getAttribute('id');
          navLinks.forEach(link => {
            const isActive = link.getAttribute('href') === `#${id}`;
            // Use the escaped colon class for consistent styling
            link.classList.toggle('\:target-current', isActive);
            link.setAttribute('aria-current', isActive);
          });
        }
      });
    }, { rootMargin: '-50% 0px -50% 0px', threshold: 0 });

    sections.forEach(section => observer.observe(section));
  }
</script>

<style>
  /* Fallback style if :target-current is not supported.
     Use :is() to combine the native pseudo-class with a fallback class for older browsers. DO NOT use `nav a:target-current, nav a.\:target-current, as the unrecognized pseudoselector invalidates the entire rule in older browsers. */
  nav a:is(:target-current, .\:target-current) {
    color: var(--accent, #007bff);
    border-bottom-color: var(--accent, #007bff);
    font-weight: bold;
  }
</style>