Ir al contenido

Accesibilidad

Lo que garantiza el paquete y lo que le queda a la app.

Es parte del contrato del paquete, no un extra. Lo que sigue es lo que el paquete garantiza y lo que le queda a la app.

Lo que garantiza el paquete

Contraste

El texto sobre el acento llega a AA 4,5:1 en claro y en oscuro. No es una revisión a ojo: un test recalcula el ratio desde OKLCH para cada marca de ejemplo y falla si una no da.

La escala de grises también está medida. En oscuro, sobre la superficie #0a0a0a:

Texto Ratio
gray-1000 #ededed (cuerpo) 16,91:1
gray-900 #a0a0a0 (secundario) 7,57:1
gray-800 #7d7d7d (el más tenue que se usa como texto) 4,81:1

gray-700 y abajo no son colores de texto: son bordes y placeholders.

Foco visible siempre

Ningún componente saca el anillo de foco. focus-visible:focus-ring en los controles, focus:focus-border en los campos, y el anillo usa brand-700. El reset también define un :focus-visible por defecto para todo lo que no sea del paquete, así que un <a> suelto de la app tampoco queda sin foco.

Semántica antes que estilo

Componente Qué emite
NavigationMenu <nav> + <ul> + <a>
DropdownMenu role="menu" / role="menuitem", recorrido por flechas
Tabs role="tablist" / tab / tabpanel con aria-controls
Switch role="switch" con aria-checked
Table <table> nativa
AlertDialog role="alertdialog"
SidebarContent <nav aria-label="Navegación principal">

Elegir mal cambia lo que anuncia un lector de pantalla. El caso más común: si los ítems navegan es NavigationMenu, si ejecutan algo es DropdownMenu. Un role="menu" con links adentro desaparece del modo de navegación por links.

Teclado

Los flotantes abren con Enter / Espacio / flechas, Escape cierra y devuelve el foco al trigger, y el fondo queda inerte mientras están abiertos. AppShell trae un link «Ir al contenido» que apunta al <main> y es la primera parada de tabulación de toda la app.

Estado anunciado

  • SidebarItem active pone aria-current="page".
  • SidebarItemBadge acepta label para que el contador se lea con contexto: «Clientes, 3 pendientes».
  • SidebarSearch shortcut emite aria-keyshortcuts.
  • Button loading pone aria-busy y aria-disabled en vez de sacar el botón del foco.
  • ComboboxStatus es una región aria-live que anuncia «Buscando…» sin robar el foco.

Movimiento

Todas las animaciones pasan por motion-reduce, además del reset global del paquete, que baja cualquier animation-duration y transition-duration a 0,01 ms cuando el sistema pide menos movimiento.

Errores de formulario

aria-invalid en el campo es lo único que hace falta: el borde rojo y el anillo de error salen de ahí, no de una clase aparte. Nunca uses una clase de color para marcar un error: el estilo y la semántica tienen que venir del mismo atributo o se desincronizan.

Lo que le queda a la app

El paquete no puede resolver esto, y es donde se rompe la accesibilidad en la práctica:

  1. Nombre accesible de los botones de ícono. size="icon-*" no tiene texto: va aria-label. Un Tooltip no sirve como nombre — en un celular no existe.
  2. Label asociado a cada campo, con htmlFor/id. Un placeholder no es una etiqueta: desaparece al escribir.
  3. El mensaje de error referenciado con aria-describedby, para que se lea al enfocar el campo.
  4. Un solo <h1> por página (PageHeaderTitle) y los <h2>/<h3> en orden, sin saltarse niveles.
  5. lang en <html>. Si la app está en español, lang="es": cambia la pronunciación del lector.
  6. Los atajos de teclado. SidebarSearch shortcut="⌘K" y DropdownMenuShortcut solo muestran y anuncian el atajo; escucharlo es de la app.
  7. role="status" en los avisos que aparecen por una acción. Un Alert que aparece solo no se anuncia.
  8. Una tarjeta interactiva tiene que ser un <a> o un <button>. Card interactive solo agrega los estilos.
  9. El color nunca solo. Un Badge verde tiene que decir «Pagado»; un Stat con trend="up" tiene que traer el signo en el número.
  10. Zoom al 200 % y ancho de 320 px sin scroll horizontal. Los componentes no lo impiden; los layouts a mano, sí.

Probarlo en cinco minutos

  1. Tabulá la pantalla entera sin tocar el mouse. Si el foco desaparece o salta a algo que no ves, hay un bug.
  2. Escape en cada flotante: tiene que cerrar y devolver el foco al trigger.
  3. Zoom al 200 %.
  4. VoiceOver (⌘F5) en el formulario principal. Cada campo tiene que anunciar su nombre, su estado y su error.
  5. Modo oscuro: el contraste no es el mismo y los grises tenues son lo primero que se cae.