Cómo compilar un componente de rutas de navegación

Una descripción general fundamental sobre cómo crear un componente de rutas de navegación responsivo y accesible para que los usuarios naveguen por tu sitio

En esta publicación, quiero compartir mi forma de pensar sobre cómo crear componentes de migas de pan. Prueba la demostración.

Demo

Si prefieres ver un video, aquí tienes una versión de YouTube de esta publicación:

Descripción general

Un componente de rutas de navegación muestra en qué parte de la jerarquía del sitio se encuentra el usuario. El nombre proviene de Hansel y Gretel, que dejaron migas de pan a su paso por un bosque oscuro y pudieron encontrar el camino a casa siguiendo el rastro de migas.

Las rutas de navegación de esta publicación no son estándares, sino similares. Ofrecen funcionalidad adicional, ya que colocan páginas hermanas directamente en la navegación con un <select>, lo que permite el acceso de varios niveles.

UX de fondo

En el video de demostración de componentes anterior, las categorías de marcadores de posición son géneros de videojuegos. Este sendero se crea navegando por la siguiente ruta: home » rpg » indie » on sale, como se muestra a continuación.

Este componente de ruta de navegación debe permitir a los usuarios moverse por esta jerarquía de información, saltando ramas y seleccionando páginas con velocidad y precisión.

Arquitectura de la información

Creo que es útil pensar en términos de colecciones y elementos.

Colecciones

Una colección es un array de opciones para elegir. En la página principal del prototipo de migas de pan de esta publicación, las colecciones son FPS, RPG, brawler, dungeon crawler, deportes y rompecabezas.

Elementos

Un videojuego es un elemento. Una colección específica también podría ser un elemento si representa a otra colección. Por ejemplo, RPG es un elemento y una colección válida. Cuando se trata de un artículo, el usuario se encuentra en esa página de la colección. Por ejemplo, están en la página de juegos de rol, que muestra una lista de juegos de rol, incluidas las subcategorías adicionales AAA, Indie y Self Published.

En términos de informática, este componente de migas de pan representa un array multidimensional:

const rawBreadcrumbData = {
  "FPS": {...},
  "RPG": {
    "AAA": {...},
    "indie": {
      "new": {...},
      "on sale": {...},
      "under 5": {...},
    },
    "self published": {...},
  },
  "brawler": {...},
  "dungeon crawler": {...},
  "sports": {...},
  "puzzle": {...},
}

Tu app o sitio web tendrá una arquitectura de la información (AI) personalizada que creará un array multidimensional diferente, pero espero que el concepto de páginas de destino de la colección y el recorrido de jerarquía también puedan incluirse en tus indicadores de ruta.

Diseños

Marca

Los buenos componentes comienzan con el código HTML adecuado. En la siguiente sección, explicaré mis opciones de marcado y cómo afectan al componente general.

Esquema oscuro y claro

<meta name="color-scheme" content="dark light">

La etiqueta meta color-scheme en el fragmento anterior le informa al navegador que esta página desea los estilos de navegador oscuro y claro. El ejemplo de pan de migas no incluye ningún CSS para estos esquemas de colores, por lo que usará los colores predeterminados que proporciona el navegador.

<nav class="breadcrumbs" role="navigation"></nav>

Es apropiado usar el elemento <nav> para la navegación del sitio, que tiene un rol de navegación implícito de ARIA. Durante las pruebas, noté que tener el atributo role cambió la forma en que un lector de pantalla interactuaba con el elemento, en realidad, se anunciaba como navegación, por lo que decidí agregarlo.

Íconos

Cuando un ícono se repite en una página, el elemento SVG <use> significa que puedes definir el path una vez y usarlo para todas las instancias del ícono. Esto evita que se repita la misma información de ruta de acceso, lo que provoca documentos más grandes y la posibilidad de incoherencias de ruta.

Para usar esta técnica, agrega un elemento SVG oculto a la página y une los íconos en un elemento <symbol> con un ID único:

<svg style="display: none;">

  <symbol id="icon-home">
    <title>A home icon</title>
    <path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
  </symbol>

  <symbol id="icon-dropdown-arrow">
    <title>A down arrow</title>
    <path d="M19 9l-7 7-7-7"/>
  </symbol>

</svg>

El navegador lee el SVG HTML, guarda la información del ícono en la memoria y continúa con el resto de la página haciendo referencia al ID para usos adicionales del ícono, de la siguiente manera:

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-home" />
</svg>

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-dropdown-arrow" />
</svg>

Herramientas para desarrolladores que muestran un elemento de uso de SVG renderizado.

Definíelos una vez y úsalos tantas veces como quieras, con un impacto mínimo en el rendimiento de la página y un diseño flexible. Observa que se agrega aria-hidden="true" al elemento SVG. Los íconos no son útiles para alguien que navega y solo escucha el contenido, por lo que ocultarlos para esos usuarios evita que agreguen ruido innecesario.

Aquí es donde divergen el pan de miga tradicional y el de este componente. Por lo general, esto solo sería un vínculo <a>, pero agregué la UX de recorrido con una selección disfrazada. La clase .crumb se encarga de distribuir el vínculo y el ícono, mientras que .crumbicon se encarga de apilar el ícono y seleccionar el elemento juntos. Lo llamé vínculo dividido porque sus funciones son muy similares a las de un botón dividido, pero para la navegación de páginas.

<span class="crumb">
  <a href="#sub-collection-b">Category B</a>
  <span class="crumbicon">
    <svg>...</svg>
    <select class="disguised-select" title="Navigate to another category">
      <option>Category A</option>
      <option selected>Category B</option>
      <option>Category C</option>
    </select>
  </span>
</span>

Un vínculo y algunas opciones no son nada especiales, pero agregan más funcionalidad a un breadcrumb simple. Agregar un title al elemento <select> es útil para los usuarios de lectores de pantalla, ya que les brinda información sobre la acción del botón. Sin embargo, también proporciona la misma ayuda a todos los demás, y verás que está en primer plano en el iPad. Un atributo proporciona contexto de botones a muchos usuarios.

Captura de pantalla en la que se muestra el elemento de selección invisible sobre el que se coloca el cursor sobre el elemento y se muestra la información sobre la herramienta contextual.

Decoraciones de separadores

<span class="crumb-separator" aria-hidden="true">→</span>

Los separadores son opcionales, y agregar solo uno también funciona muy bien (consulta el tercer ejemplo en el video anterior). Luego, le doy a cada aria-hidden="true", ya que son decorativos y no es algo que un lector de pantalla necesite anunciar.

La propiedad gap, que se explica a continuación, simplifica el espaciado de estos.

Estilos

Dado que el color usa colores del sistema, se trata principalmente de espacios y pilas para los estilos.

Dirección y flujo del diseño

DevTools muestra la alineación de la navegación de ruta de navegación con su función de superposición de flexbox.

El elemento de navegación principal nav.breadcrumbs establece una propiedad personalizada con alcance para que usen los elementos secundarios y, de lo contrario, establece un diseño horizontal alineado de forma vertical. Esto garantiza que los indicadores de ruta, los divisores y los íconos se alineen.

.breadcrumbs {
  --nav-gap: 2ch;

  display: flex;
  align-items: center;
  gap: var(--nav-gap);
  padding: calc(var(--nav-gap) / 2);
}

Una ruta de navegación que se muestra alineada verticalmente con superposiciones de flexbox.

Cada .crumb también establece un diseño horizontal alineado verticalmente con un poco de espacio, pero se orienta especialmente a sus elementos secundarios de vínculo y especifica el estilo white-space: nowrap. Esto es fundamental para las rutas de navegación de varias palabras, ya que no queremos que sean de varias líneas. Más adelante en esta publicación, agregaremos diseños para controlar el desbordamiento horizontal que generó esta propiedad white-space.

.crumb {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--nav-gap) / 4);

  & > a {
    white-space: nowrap;

    &[aria-current="page"] {
      font-weight: bold;
    }
  }
}

Se agrega aria-current="page" para ayudar a que el vínculo de la página actual se destaque del resto. Los usuarios de lectores de pantalla no solo tendrán un indicador claro de que el vínculo es para la página actual, sino que también le aplicamos un diseño visual al elemento para ayudar a los usuarios videntes a obtener una experiencia del usuario similar.

El componente .crumbicon usa la cuadrícula para apilar un ícono SVG con un elemento <select> "casi invisible".

DevTools de cuadrícula que se muestra superpuesta en un botón en el que la fila y la columna se denominan pila.

.crumbicon {
  --crumbicon-size: 3ch;

  display: grid;
  grid: [stack] var(--crumbicon-size) / [stack] var(--crumbicon-size);
  place-items: center;

  & > * {
    grid-area: stack;
  }
}

El elemento <select> es el último en el DOM, por lo que se ubica en la parte superior de la pila y es interactivo. Agrega un diseño de opacity: .01 para que el elemento aún se pueda usar y el resultado sea una casilla de selección que se ajuste perfectamente a la forma del ícono. Esta es una buena manera de personalizar el aspecto de un elemento <select> y, al mismo tiempo, mantener la funcionalidad integrada.

.disguised-select {
  inline-size: 100%;
  block-size: 100%;
  opacity: .01;
  font-size: min(100%, 16px); /* Defaults to 16px; fixes iOS zoom */
}

Menú ampliado

El pan de miga debe poder representar un recorrido muy largo. Me gusta permitir que las cosas salgan de la pantalla horizontalmente, cuando corresponde, y sentí que este componente de las rutas de navegación funcionó bien.

.breadcrumbs {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: calc(var(--nav-gap) / 2);

  & > .crumb:last-of-type {
    scroll-snap-align: end;
  }

  @supports (-webkit-hyphens:none) { & {
    scroll-snap-type: none;
  }}
}

Los estilos de menú ampliado configuran la siguiente UX:

  • Desplazamiento horizontal con contención de desplazamiento excesivo.
  • Margen de desplazamiento horizontal.
  • Un punto de ajuste en el último fragmento Esto significa que, cuando se carga la página, el primer fragmento se carga ajustado y en la vista.
  • Quita el punto de ajuste de Safari, que tiene dificultades con el desplazamiento horizontal y las combinaciones de efectos de ajuste.

Consultas de medios

Un ajuste sutil para los viewports más pequeños es ocultar la etiqueta "Inicio" y dejar solo el ícono:

@media (width <= 480px) {
  .breadcrumbs .home-label {
    display: none;
  }
}

Comparación de los indicadores de ruta con y sin una etiqueta de página principal

Accesibilidad

Movimiento

No hay mucho movimiento en este componente, pero si unes la transición en una verificación prefers-reduced-motion, podemos evitar el movimiento no deseado.

@media (prefers-reduced-motion: no-preference) {
  .crumbicon {
    transition: box-shadow .2s ease;
  }
}

No es necesario cambiar ninguno de los otros estilos, los efectos de desplazamiento y enfoque son geniales y significativos sin un transition, pero si el movimiento está bien, agregaremos una transición sutil a la interacción.

JavaScript

En primer lugar, independientemente del tipo de router que uses en tu sitio o aplicación, cuando un usuario cambia el pan de breadcrumbs, se debe actualizar la URL y se le debe mostrar la página adecuada. En segundo lugar, para normalizar la experiencia del usuario, asegúrate de que no se produzcan navegaciones inesperadas cuando los usuarios solo estén explorando las opciones de <select>.

JavaScript debe controlar dos medidas críticas de la experiencia del usuario: la selección cambió y la prevención de activación de eventos de cambio <select> rápidos.

La prevención de eventos anticipados es necesaria debido al uso de un elemento <select>. En Windows Edge y, probablemente, en otros navegadores, se activa el evento changed de selección a medida que el usuario explora las opciones con el teclado. Por eso, la llamé "ansiosa", ya que el usuario solo pseudoseleccionó la opción, como un desplazamiento del mouse o un enfoque, pero aún no confirmó la elección con enter o click. El evento inmediato hace que la función de cambio de categoría de componente sea inaccesible, ya que, cuando se abre el cuadro de selección y simplemente explora un elemento, se activará el evento y cambiará la página, antes de que el usuario esté listo.

Un evento mejor de <select> modificado

const crumbs = document.querySelectorAll('.breadcrumbs select')
const allowedKeys = new Set(['Tab', 'Enter', ' '])
const preventedKeys = new Set(['ArrowUp', 'ArrowDown'])

// watch crumbs for changes,
// ensures it's a full value change, not a user exploring options via keyboard
crumbs.forEach(nav => {
  let ignoreChange = false

  nav.addEventListener('change', e => {
    if (ignoreChange) return
    // it's actually changed!
  })

  nav.addEventListener('keydown', ({ key }) => {
    if (preventedKeys.has(key))
      ignoreChange = true
    else if (allowedKeys.has(key))
      ignoreChange = false
  })
})

La estrategia para esto es observar los eventos de tecla presionada en cada elemento <select> y determinar si la tecla presionada fue la confirmación de navegación (Tab o Enter) o la navegación espacial (ArrowUp o ArrowDown). Con esta determinación, el componente puede decidir esperar o ir cuando se activa el evento del elemento <select>.

Conclusión

Ahora que sabes cómo lo hice, ¿cómo lo harías tú? 🙂

Diversifiquemos nuestros enfoques y aprendamos todas las formas de compilar en la Web. Crea una demo, twittea los vínculos y los agregaré a la sección de remixes de la comunidad a continuación.

Remixes de la comunidad