Cómo crear una navegación con Scrollspy

Una navegación con scrollspy es un patrón de IU común que destaca automáticamente el vínculo de navegación correspondiente a la sección de la página que se encuentra actualmente en el viewport. Esto les proporciona a los usuarios una idea clara de dónde se encuentran en un documento largo mientras se desplazan.

Este patrón es común en dos situaciones:

  • Sitios web de una sola página: Son aquellos en los que los vínculos de navegación principales del sitio dirigen a diferentes secciones de una sola página, en lugar de a URLs independientes.
  • Contenido de formato largo: Es cuando un índice o una navegación en la barra lateral hace un seguimiento del progreso del usuario en un artículo, una guía o una documentación.

Cómo implementarlo

Tradicionalmente, el patrón de scrollspy requería JavaScript y la API de IntersectionObserver. El CSS moderno proporciona una forma nativa de lograrlo con la propiedad scroll-target-group y la pseudoclase :target-current.

  1. Asegúrate de que los vínculos de navegación usen identificadores de fragmentos: Tu navegación debe consistir en vínculos de anclaje (<a>) que dirijan a secciones de la misma página con identificadores de fragmentos (p.ej., <a href="#section-1">) que coincidan con el id de tus secciones de contenido.
  2. Habilita la agrupación de objetivos de desplazamiento: Aplica la propiedad scroll-target-group: auto al contenedor de tus vínculos de navegación. Esto le indica al navegador que trate estos vínculos como marcadores que hacen un seguimiento de sus respectivos objetivos.
  3. Estiliza el vínculo activo: Usa la seudoclase :target-current en tus vínculos de navegación para aplicar estilos cuando su sección de destino esté visible en el contenedor de desplazamiento.
  4. Estado semántico: Usa una secuencia de comandos para actualizar aria-current="true" en el vínculo activo cuando se detenga el desplazamiento para garantizar que el estado se comunique correctamente a las tecnologías de accesibilidad.

Ejemplo de código

En este ejemplo, se destaca el vínculo activo en un menú de navegación fijo a medida que el usuario se desplaza por las secciones.


/* 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ácticas recomendadas

  • SÍ uses scroll-target-group: auto en un contenedor exclusivo para vínculos de navegación: Esto garantiza que el navegador sepa de qué vínculos se debe hacer un seguimiento.
  • SÍ asegúrate de que los destinos tengan atributos id únicos que coincidan con los atributos href de los vínculos.
  • SÍ proporciones suficiente espacio vertical para las secciones: scroll-target-group identifica con mayor claridad el objetivo visible cuando las secciones son lo suficientemente grandes como para que solo se vean una o dos a la vez.
  • OBLIGATORIO: Para la accesibilidad, el estado visual debe reflejarse con aria-current. Si bien :target-current controla el aspecto visual, este no se expone a los lectores de pantalla.
  • AVOID depender únicamente del color para el estado activo: Incluye otros indicadores visuales, como el grosor de la fuente, un subrayado o un punto indicador, o asegúrate de que haya suficiente contraste entre los colores para que la diferencia sea accesible para los usuarios con daltonismo.

Estrategias de resguardo

En el caso de los navegadores que no admiten scroll-target-group, debes usar un resguardo basado en la API de 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>