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 las ideas sobre una forma de crear componentes de ruta de navegación. Prueba la demostración.

Demostración

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 es de Hansel y Gretel, que arrojaron migas detrás de ellos en algunos bosques oscuros y pudieron encontrar el camino a casa rastreando migas al revés.

Las rutas de navegación de esta publicación no son estándares, sino que se parecen a las rutas de navegación. Ofrecen una funcionalidad adicional, ya que colocan páginas del mismo nivel directamente en la navegación con un <select>, lo que permite el acceso a varios niveles.

UX de segundo plano

En el video de demostración de componentes anterior, las categorías de marcadores de posición son géneros de videojuegos. Para crear este sendero, se utiliza 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 y saltar ramas y seleccionar páginas con velocidad y precisión.

Arquitectura de la información

Me parece útil pensar en términos de colecciones y elementos.

Colecciones

Una colección es un array de opciones para elegir. De la página principal del prototipo de ruta de navegación de esta publicación, las colecciones son de FPS, RPG, peleadores, rastreadores de mazmorras, deportes y rompecabezas.

Ítems

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

En términos informáticos, este componente de ruta de navegación representa un arreglo 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 (IA) personalizada que creará un array multidimensional diferente, pero esperamos que el concepto de las páginas de destino de las colecciones y el recorrido de la jerarquía también puedan incluirse en tus rutas de navegación.

Diseños

Marca

Los componentes adecuados comienzan con el código HTML adecuado. En esta sección, explicaré mis opciones de lenguaje de marcado y cómo afectan el componente general.

Esquema oscuro y claro

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

La metaetiqueta color-scheme del fragmento anterior informa al navegador que esta página desea los estilos de navegador claro y oscuro. Las rutas de navegación de ejemplo no incluyen ningún CSS para estos esquemas de colores, por lo que se usarán 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 ARIA implícito. 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 anunció como navegación, por lo que decidí agregarlo.

Íconos

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

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 HTML del SVG, coloca la información del ícono en la memoria y continúa con el resto de la página que hace 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.

Define una vez y úsalo todas las veces que quieras, con un impacto mínimo en el rendimiento de la página y un estilo flexible. Ten en cuenta que se agrega aria-hidden="true" al elemento SVG. Los íconos no son útiles para una persona de navegación que solo escucha el contenido, ya que si los ocultas de esos usuarios, se evita que agregue ruido innecesario.

Aquí es donde divergen la ruta de navegación tradicional y las de este componente. Por lo general, este solo sería un vínculo <a>, pero agregué una UX de recorrido con una selección disfrazada. La clase .crumb se encarga de diseñar el vínculo y el ícono, mientras que .crumbicon es responsable de apilar el ícono y seleccionar el elemento juntos. Lo llamamos vínculo dividido porque sus funciones son muy similares a 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 especial, pero agregan más funcionalidad a una ruta de navegación simple. Agregar un title al elemento <select> es útil para los usuarios de lectores de pantalla, ya que les proporciona información sobre la acción del botón. Sin embargo, también les brinda la misma ayuda a los demás, aparecerá en primer plano en el iPad. Un atributo proporciona contexto sobre los botones a muchos usuarios.

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

Decoraciones para separadores

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

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

La propiedad gap, que se aborda a continuación, hace que el espaciado de estos sea sencillo.

Estilos

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

Dirección y flujo del diseño

Herramientas para desarrolladores que muestran la alineación de la 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 la usen los elementos secundarios; de lo contrario, establece un diseño horizontal alineado verticalmente. Esto garantiza que se alineen las migas, los divisores y los iconos.

.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 algún espacio, pero se orienta en especial a sus elementos secundarios de vínculo y especifica el estilo white-space: nowrap. Esto es crucial para las rutas de navegación de varias palabras, ya que no queremos que tengan varias líneas. Más adelante en esta entrada, 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. No solo los usuarios de lectores de pantalla tendrán un indicador claro de que el vínculo es para la página actual, sino que también diseñamos visualmente el elemento para ayudar a los usuarios videntes a tener una experiencia del usuario similar.

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

Se muestran las Herramientas para desarrolladores de cuadrícula superpuestas a 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 está en la parte superior de la pila y es interactivo. Agrega un estilo de opacity: .01 para que el elemento aún se pueda usar y, como resultado, aparezca un cuadro 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

Las rutas de navegación deberían poder representar un recorrido muy largo. Me encanta permitir que las cosas se salgan de la pantalla horizontalmente, cuando corresponda, y sentí que este componente de la ruta de navegación califica 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 desbordamiento configuran la siguiente UX:

  • Desplazamiento horizontal con contención de sobredesplazamiento.
  • Padding horizontal de desplazamiento
  • Un punto de chasquido en la última miga. Esto significa que, en la carga de la página, se carga la primera ruta ajustada y a la vista.
  • Se quitó el punto de ajuste de Safari, que tiene dificultades con las combinaciones de efectos de ajuste y desplazamiento horizontal.

Consultas de medios

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

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

Una al lado de las otras migas de pan con y sin la etiqueta de inicio, para compararlas.

Accesibilidad

Movimiento

Este componente no tiene mucho movimiento, pero, si unimos la transición a una verificación de 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 excelentes 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 las rutas de navegación, la URL debe actualizarse y el usuario debe mostrarle 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 exploren las opciones de <select>.

Dos medidas fundamentales de la experiencia del usuario que controlará JavaScript: la selección cambió y la prevención de activación de eventos de <select> cambió con anticipación.

La prevención de eventos inmediatos es necesaria debido al uso de un elemento <select>. En Windows Edge, y probablemente en otros navegadores también, se activa el evento changed seleccionado a medida que el usuario explora las opciones con el teclado. Por eso, lo llamé inmediatamente, ya que el usuario solo pseudoselecciona la opción, como colocar el cursor sobre ella o el enfoque, pero no confirma la elección con enter o click. El evento inmediato hace que esta función de cambio de categoría de componente sea inaccesible, ya que abrir el cuadro de selección y simplemente explorar un elemento activará el evento y cambiará la página, antes de que el usuario esté listo.

Cambió un evento mejor <select>

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 pulsación del teclado en cada elemento <select> y determinar si la tecla que se presionó fue confirmación de navegación (Tab o Enter) o navegación espacial (ArrowUp o ArrowDown). Con esta determinación, el componente puede decidir esperar o ir cuando se activa el evento para el elemento <select>.

Conclusión

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

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

Remixes de la comunidad