Código listo para copiar
Navegación con CSS moderno
Cinco piezas que resuelven cómo se mueve la gente por una app o un sitio, casi todas sin JavaScript: el navegador ya sabe plegar, abrir cajones y atrapar el foco si le pides lo correcto. Cada una trae su estado activo, su foco visible por teclado y su respeto a quien pide menos movimiento.
Laterales
Para apps y paneles: un rail de iconos que crece al pasar el cursor, y un menú completo con grupos que se pliegan con el elemento details.
Menú de app con grupos plegables
Móvil y pestañas
Para sitios: el cajón que se abre con el atributo popover, la barra inferior de móvil con su zona segura, y las pestañas con un solo indicador que se desliza.
Cajón de navegación con popover
Barra inferior móvil
Pestañas con indicador deslizante
Por qué casi todo es CSS puro
Hace unos años, cada una de estas piezas pedía su script: abrir y cerrar un
menú, saber qué pestaña está activa, plegar un grupo. Hoy el navegador trae
esas tres cosas resueltas. :has() deja que un contenedor reaccione
a lo que pasa dentro de él (una casilla marcada, un enlace bajo el cursor);
<details> pliega y despliega con teclado y lector de pantalla
incluidos; y el atributo popover abre un panel en la capa superior,
lo cierra con Escape o con un clic fuera y pinta el fondo con
::backdrop. Cuatro de las cinco piezas no llevan ni una línea de JS.
Cómo usarlas
- Copia CSS y HTML de la pieza. Los iconos van en línea como SVG de trazo: cámbialos por los tuyos manteniendo el tamaño.
- Marca la página actual con
aria-current="page"en el enlace que toque. Es lo que pinta el estado activo y lo que anuncia el lector de pantalla. - Los colores están escritos en la paleta de Lienzo (tinta, violeta, turquesa). Sustitúyelos por tus variables y el resto se mantiene.
Accesibilidad que ya viene puesta
Cada enlace tiene su foco visible con :focus-visible, los grupos
plegados quedan fuera del orden de tabulación porque details lo
hace solo, y el cajón devuelve el foco al botón que lo abrió porque
popover también. El rail se abre al recibir foco por teclado, no
solo con el cursor: una persona que navega con Tab ve las etiquetas igual que
quien usa ratón.
Dónde encajan
- Paneles y herramientas: el rail cuando el espacio manda y el menú de app cuando hay muchas secciones.
- Sitios en móvil: el cajón para el menú principal y la barra inferior para las cuatro o cinco acciones de siempre.
- Secciones de una misma página: las pestañas con indicador, que también sirven como filtro de una galería.
Las demás categorías
La familia completa, cada una con sus piezas y su código: formas y recortes, vidrio y neón, bento grid, botones y controles, tarjetas, formularios, datos y métricas, microinteracciones y estados de carga. Y si buscas generadores en vez de piezas hechas, están en Código web.
Preguntas frecuentes
¿Funciona :has() en todos los navegadores?
Sí en todos los actuales: Chrome, Edge, Firefox y Safari lo llevan desde 2023 y es Baseline. Si necesitas cubrir un navegador de hace más de tres años, el rail y las pestañas siguen viéndose bien: solo pierden el estado que depende de la casilla marcada.
¿Y el atributo popover?
También es Baseline desde 2024 en los cuatro grandes. En un navegador sin soporte el cajón se comporta como un bloque normal, visible, sin romper la página. Si te importa ese caso, escóndelo con una regla @supports not selector(:popover-open).
¿Por qué el rail se abre también con el teclado?
Porque :focus-within detecta que un enlace de dentro tiene el foco. Sin esa regla, quien navega con Tab vería solo iconos sin etiqueta: el rail sería inutilizable para esa persona. Es una línea de CSS y evita un problema real.
¿Cómo cambio el número de pestañas?
Ajusta la variable --n al número de enlaces y añade una regla :has(> a:nth-child(N)...) por cada pestaña nueva, copiando el patrón de las que hay. El indicador calcula su ancho a partir de --n, así que no hay que tocar nada más.
¿El menú de app recuerda qué grupos están plegados?
No, es CSS puro: al recargar vuelve al estado del HTML (los grupos con el atributo open empiezan abiertos). Si quieres memoria, guarda el estado de cada details en localStorage con tres líneas de JS al evento toggle.
¿Puedo usar la barra inferior en una web normal, no solo en una app?
Sí, y de hecho es donde más se nota: en móvil, una barra fija con cuatro o cinco destinos ahorra el viaje al menú superior. Añade la clase --fija y respeta la zona segura de los teléfonos con muesca, que la receta ya contempla con env(safe-area-inset-bottom).
Si conoces a alguien que maqueta webs, esto le ahorra el día.