Icono del sitio IT&ライフハックブログ|学びと実践のためのアイデア集

Guía completa de accesibilidad para interacción con teclado y gestión del foco: orden, visibilidad, Roving TabIndex, atajos y patrones para modales/pestañas/menús

brown wooden destination arrow guide

Photo by Pixabay on Pexels.com

Guía completa de accesibilidad para interacción con teclado y gestión del foco: orden, visibilidad, Roving TabIndex, atajos y patrones para modales/pestañas/menús

Resumen (puntos clave primero)

  • Mantén el orden de Tab = orden lógico, y añade un enlace de salto en la primera pulsación de Tab → contenido principal, para que las personas tengan un “mapa”.
  • Haz los indicadores de foco gruesos y con alto contraste; usa :focus-visible. Cumple el requisito de contraste no textual de 3:1.
  • Para UIs compuestas (pestañas/menús/cuadrículas), usa Roving TabIndex: Tab = moverse entre regiones / Flechas = moverse dentro de una región.
  • Los modales deben cubrir los cuatro esenciales: abrir, trampa de foco, Esc para cerrar, restaurar el foco. Silencia el fondo con inert/aria-hidden.
  • Para atajos, decláralos y evita conflictos. Ofrece siempre operaciones alternativas (clic/teclado estándar) y haz que el esquema sea fácil de aprender.
  • Incluye una prueba rápida de 5 minutos y muchos fragmentos listos para producción (HTML/ARIA/CSS/JS).

Audiencia (concreta): Ingenieros front-end, diseñadores UI/UX, PMs, QA, redactores técnicos y responsables de design systems
Nivel de accesibilidad: WCAG 2.1 AA (con elementos recomendados de 2.2/2.5 cuando aplique)


1. Introducción: la accesibilidad empieza con el “viaje del foco”

Las personas navegan con la tecla Tab por enlaces y botones, usan las flechas para elegir elementos y Enter/Espacio para activar. Más allá de los lectores de pantalla, la web se usa con gran variedad de métodos de entrada: trackpads, teclados externos, punteros bucales y conmutadores.
Foco invisible, orden de foco caótico o modales que no se pueden cerrar son estresantes para cualquiera. Cuando se alinean orden, visibilidad y consistencia operativa, la experiencia se vuelve mucho más tranquila.
Iremos de principios → patrones comunes → código → pruebas → operación, con plantillas que puedes usar de inmediato.


2. Principios básicos: Orden, Nombre, Percepción

2.1 Orden de Tab = orden lógico

  • Orden del DOM = orden de lectura = orden de Tab como base. Si reordenas visualmente, prioriza el orden del código fuente y cambia solo el layout con CSS (p. ej., order).
  • Usa tabindex="0" para hacer elementos focalizables. Evita valores positivos (1, 2, …): rompen el orden.

2.2 Nombre, Rol, Valor (NRV)

  • Nombre: La etiqueta visible = nombre accesible (WCAG 2.5.3 “Label in Name”).
  • Rol: Usa roles nativos o ARIA equivalentes (button, tab, tabpanel, menuitem, etc.).
  • Valor: Expón estado con aria-checked / aria-expanded / aria-selected / aria-valuenow, etc.

2.3 Hacer el foco “visible”

  • Usa ≥ 3px de grosor, 2–3px de separación y garantiza contraste con el fondo.
  • Muestra un indicador fuerte solo para teclado con :focus-visible; mantén sutil el foco por ratón.
:focus-visible {
  outline: 3px solid #FF9900; 
  outline-offset: 3px;
}

3. Enlaces de salto y “landmarks”: acorta el primer paso

  • Pon un enlace de salto en la parte superior para saltar al contenido de main.
  • Para múltiples regiones nav/aside, añade aria-label (“Global”, “Pie”, etc.) para desambiguar.
<a class="skip" href="#content">Saltar al contenido principal</a>
<main id="content" tabindex="-1">…</main>
.skip{ position:absolute; left:-9999px; }
.skip:focus{ left:1rem; top:1rem; background:#111; color:#fff; padding:.5rem .75rem; border-radius:.5rem; }

4. Roving TabIndex: el predeterminado para componentes compuestos

Objetivo: Mantener corta la secuencia de Tab usando Tab para moverse entre componentes y flechas para moverse dentro de ellos.

4.1 Pestañas (Tabs)

  • Estructura: role="tablist" con button[role="tab"], más div[role="tabpanel"] emparejados.
  • Teclado: Flechas Izq./Der. para pestañas adyacentes, Home/End para saltar, Espacio/Enter para seleccionar.
<div role="tablist" aria-label="Facturación" id="tabs">
  <button role="tab" aria-selected="true"  aria-controls="p1" id="t1" tabindex="0">Extractos</button>
  <button role="tab" aria-selected="false" aria-controls="p2" id="t2" tabindex="-1">Métodos de pago</button>
  <button role="tab" aria-selected="false" aria-controls="p3" id="t3" tabindex="-1">Ajustes</button>
</div>
<section id="p1" role="tabpanel" aria-labelledby="t1">…</section>
<section id="p2" role="tabpanel" aria-labelledby="t2" hidden>…</section>
<section id="p3" role="tabpanel" aria-labelledby="t3" hidden>…</section>
const tabs = document.querySelectorAll('[role="tab"]');
const panels = document.querySelectorAll('[role="tabpanel"]');

function activateTab(tab){
  tabs.forEach(t=>{ t.setAttribute('tabindex', t===tab? '0':'-1'); t.setAttribute('aria-selected', String(t===tab)); });
  panels.forEach(p=> p.hidden = (p.id !== tab.getAttribute('aria-controls')));
  tab.focus();
}
document.getElementById('tabs').addEventListener('keydown', e=>{
  const list = [...tabs]; const i = list.indexOf(document.activeElement);
  if(e.key==='ArrowRight') activateTab(list[(i+1)%list.length]);
  if(e.key==='ArrowLeft')  activateTab(list[(i-1+list.length)%list.length]);
  if(e.key==='Home')       activateTab(list[0]);
  if(e.key==='End')        activateTab(list[list.length-1]);
});
tabs.forEach(t=> t.addEventListener('click', ()=>activateTab(t)));

4.2 Menú (navegación tipo “disclosure”)

  • El disparador es un <button aria-expanded>. Al abrir, pon tabindex="0" en el primer elemento.
  • Usa flechas para moverte; Esc cierra y devuelve el foco al disparador.
<div class="menu">
  <button id="menuBtn" aria-expanded="false" aria-controls="m1">Cuenta</button>
  <ul id="m1" role="menu" hidden>
    <li><a role="menuitem" href="/perfil" tabindex="0">Perfil</a></li>
    <li><a role="menuitem" href="/ajustes" tabindex="-1">Ajustes</a></li>
    <li><button role="menuitem" tabindex="-1">Cerrar sesión</button></li>
  </ul>
</div>
const btn = document.getElementById('menuBtn'), menu = document.getElementById('m1');
const items = ()=> [...menu.querySelectorAll('[role="menuitem"]')];
btn.addEventListener('click', ()=>{
  const open = btn.getAttribute('aria-expanded')==='true';
  btn.setAttribute('aria-expanded', String(!open));
  menu.hidden = open;
  if(!open) items()[0].focus();
});
menu.addEventListener('keydown', e=>{
  const list = items(); const i = list.indexOf(document.activeElement);
  if(e.key==='ArrowDown') list[(i+1)%list.length].focus();
  if(e.key==='ArrowUp')   list[(i-1+list.length)%list.length].focus();
  if(e.key==='Escape'){ btn.click(); btn.focus(); }
});

Nota: Para la navegación del sitio, ul/li/a nativos con un botón para mostrar/ocultar suelen ser suficientes. Evita sobreusar role="menu", que implica menús de estilo aplicación.

4.3 Grid (conjuntos de tarjetas, date pickers)

  • Usa role="grid" con role="gridcell" para celdas. Implementa navegación por flechas por fila/columna y Enter/Espacio para seleccionar.

5. Diálogos modales: abrir, atrapar, restaurar, anunciar

5.1 Los cuatro esenciales

  1. Abrir: Mueve el foco al primer elemento accionable.
  2. Atrapar: Cicla Tab dentro del diálogo; vuelve el fondo inert (alternativa: aria-hidden="true").
  3. Esc: Cierra con Escape.
  4. Restaurar: Devuelve el foco al elemento disparador al cerrar.
<button id="open">Abrir ajustes</button>
<dialog id="dlg" aria-labelledby="dlg-title">
  <h2 id="dlg-title">Notificaciones</h2>
  <label><input type="checkbox"> Recibir notificaciones por correo</label>
  <div class="actions">
    <button id="save">Guardar</button>
    <button id="close">Cerrar</button>
  </div>
</dialog>
const openBtn = document.getElementById('open');
const dlg = document.getElementById('dlg'); 
let lastFocus;
openBtn.addEventListener('click', ()=>{
  lastFocus = document.activeElement;
  dlg.showModal(); // Proporciona fallback para entornos sin <dialog>
  dlg.querySelector('input,button,select,textarea,[tabindex="0"]')?.focus();
});
document.getElementById('close').addEventListener('click', ()=>{ dlg.close(); lastFocus?.focus(); });
dlg.addEventListener('cancel', (e)=>{ e.preventDefault(); dlg.close(); lastFocus?.focus(); }); // Esc

Si <dialog> no está soportado, añade aria-hidden="true" al fondo, aplica body { overflow: hidden; }, y crea una trampa de foco dentro del diálogo.


6. Diseño de foco visible: que nadie lo pierda

  • Cumple el 3:1 de contraste no textual para anillos, iconos y contornos.
  • Grosor: 2–3px; separación: 2–4px para evitar “sangrado”.
  • Provee colores para tema claro/oscuro; refuerza con prefers-contrast: more.
:root{ --focus:#FF9900; }
:focus-visible{ outline:3px solid var(--focus); outline-offset:3px; }
@media (prefers-contrast: more){
  :focus-visible{ outline-width:4px; }
}

7. Diseño de atajos: rápidos, pero siempre aprendibles y sustituibles

  • Ofrece siempre operaciones alternativas (botones/menús/teclado estándar).
  • Unifica notación (Win/Linux = Ctrl, macOS = ⌘).
  • Evita conflictos con combinaciones reservadas del navegador (Ctrl+L, Ctrl+T, ⌘+R, etc.).
  • Muestra los atajos en ayuda, tooltips y menús.
<button aria-keyshortcuts="Ctrl+K" aria-label="Buscar (Ctrl+K)">Buscar</button>
window.addEventListener('keydown', (e)=>{
  const mac = navigator.platform.includes('Mac');
  if((mac? e.metaKey : e.ctrlKey) && e.key.toLowerCase()==='k'){
    e.preventDefault();
    document.getElementById('search').focus();
  }
});

8. Patrones de orden de foco: recetario práctico

8.1 Lista de tarjetas → detalle

  • Región (role="region")enlace del encabezado de tarjetaacciones primarias internas → siguiente tarjeta.
  • Coloca enlaces “Más” al final. Repite encabezado → cuerpo → acciones de forma consistente.

8.2 Filtros y resultados

  • Pon los filtros primero (y un enlace de salto para ir a resultados).
  • Tras aplicar, mueve el foco a resultados y anuncia con role="status".

8.3 En errores

  • Mueve el foco a un resumen de errores (role="alert"), con enlaces a los campos.

9. Alternativas para UIs centradas en ratón/táctil: arrastre, hover, pulsación larga

  • Alternativas al arrastre: Proporciona botones/menús (arriba/abajo/izq./der.) para lograr el mismo resultado (WCAG 2.5.7).
  • El contenido solo en hover debe aparecer también en foco (tooltips, mega-menús).
  • La pulsación larga debe tener alternativas clic/Enter y tolerancias de tiempo relajadas.
<div role="listbox" aria-label="Orden">
  <button aria-label="Subir uno">↑</button>
  <button aria-label="Bajar uno">↓</button>
</div>

10. Regiones en vivo y anuncios de estado: silenciosos pero fiables

  • role="status": Actualizaciones no interruptivas como “Guardado”.
  • role="alert": Críticas e interruptivas.
  • aria-live="polite": Info que se actualiza automáticamente (conteos de búsqueda).
<div id="status" role="status" aria-atomic="true" class="sr-only"></div>
<script>
function saved(){ document.getElementById('status').textContent='Guardado correctamente.'; }
</script>

11. Errores comunes y cómo evitarlos

Error Qué ocurre Cómo evitar
Abuso de tabindex positivo Se rompe el orden; pérdida de foco Rediseña el DOM; usa solo tabindex="0"
Quitar los anillos de foco Las personas pierden dónde están Mantenlos, con estilo vía :focus-visible
Fondo operable en modales Doble foco; confusión inert/aria-hidden + trampa + Esc + restaurar
Pestañas sin flechas Mayor coste de aprendizaje Roving TabIndex + Izq./Der. + Home/End
Solo hover para mostrar Inaccesible con teclado Botón conmutador + aria-expanded
Ocultar diálogo solo con display:none Desfase con lectores de pantalla Define atributos adecuados y movimientos de foco al alternar
Acciones solo con atajos Inaprendible Duplica con menús/botones/ayuda

12. Prueba rápida de 5 minutos: el ritual mínimo por release

  1. Completa flujos clave usando solo Tab (abrir → operar → cerrar → siguiente).
  2. El primer Tab muestra un enlace de salto que aterriza en el contenido principal.
  3. Pestañas/menús: flechas mueven dentro, y Home/End funcionan.
  4. Modal: abre → foco inicial dentro → Esc → foco restaurado al disparador.
  5. El anillo de foco es siempre visible (tema claro/oscuro).
  6. Atajos: listados en ayuda, alternativas disponibles, sin conflictos con atajos del navegador.
  7. Con lector de pantalla (NVDA/VoiceOver), Nombre/Rol/Valor se anuncian correctamente.

13. Biblioteca de snippets (copiar/pegar)

13.1 Tokens de focus-visible

:root{ --focus-color:#FF9900; --focus-width:3px; --focus-offset:3px; }
:focus-visible{ outline: var(--focus-width) solid var(--focus-color); outline-offset: var(--focus-offset); }

13.2 Fallback de inert (simple)

function setInert(el, inert){
  if('inert' in HTMLElement.prototype){ el.inert = inert; }
  else { el.setAttribute('aria-hidden', String(inert)); }
}

13.3 Movimiento con flechas en una grid (esqueleto)

function moveInGrid(e, cols){
  const items = [...document.querySelectorAll('[role="gridcell"]')];
  const i = items.indexOf(document.activeElement);
  const map = { ArrowRight: i+1, ArrowLeft: i-1, ArrowDown: i+cols, ArrowUp: i-cols };
  if(map[e.key] !== undefined){
    items[(map[e.key]+items.length)%items.length]?.focus();
    e.preventDefault();
  }
}

13.4 Recordar y restaurar el “punto de retorno”

let lastFocus;
function rememberFocus(){ lastFocus = document.activeElement; }
function restoreFocus(){ lastFocus?.focus(); }

14. Caso práctico: reducir churn corrigiendo solo el modelo de foco

Antes

  • Se eliminó el anillo de foco.
  • Mega-menú que solo abría con hover; inaccesible por teclado.
  • El modal permitía interactuar con el fondo; Esc no hacía nada.

Después

  1. Se introdujo un contorno grueso con :focus-visible.
  2. Se rehízo el mega-menú como botón de “disclosure” + navegación con flechas.
  3. El modal ahora atrae el foco + Esc cierra + foco restaurado, con inert en el fondo.
    Resultado: Finalización de formularios +14%, reportes de “me pierdo” con lector −76%, visitas al help center −28%.

15. Despliegue organizacional: codifica “reglas de interacción” en tu design system

  • Añade una tabla de interacción con teclado (Tab/Flechas/Home/End/Esc/Enter/Espacio) a cada especificación de componente.
  • Define tokens de foco (color/grosor/separación) globalmente.
  • Extiende tu Definition of Done con: enlace de salto, orden de foco, cuatro esenciales del modal, Roving TabIndex y alternativas de atajos.
  • Añade checks automatizados + prueba de 5 minutos a los PRs para minimizar regresiones.

16. Audiencia y beneficios concretos

  • Ingeniería front-end: Estandarizando Roving TabIndex y control de modales, la implementación se estabiliza y bajan las regresiones.
  • Diseño UI/UX: Un lenguaje de foco visible equilibra accesibilidad y marca.
  • PMs/Directores: Los criterios de aceptación AA se vuelven explícitos: puertas de calidad claras.
  • QA/CS: Se estandarizan perspectivas de prueba; bajan los tickets de “me perdí”.
  • Usuarios: Con teclado/AT/conmutador, pueden terminar tareas con confianza.

17. Instantánea de conformidad (lo que entrega este artículo)

  • WCAG 2.1 AA (puntos principales)
    • 2.1.1 Teclado: Todas las funciones operables con teclado
    • 2.4.3 Orden del foco: Orden lógico preservado
    • 2.4.7 Foco visible: Anillo claro por grosor/color/separación
    • 2.4.1 Omitir bloques: Enlace de salto
    • 4.1.2 Nombre, Rol, Valor: Estado/relaciones vía aria-*
    • 1.4.11 Contraste no textual: 3:1 para anillos de foco y límites
    • 2.2.2 Pausar, Detener, Ocultar: Apertura/cierre controlados en modales/menús
  • WCAG 2.2 (recomendado)
    • 2.5.7 Movimientos de arrastre: Alternativas al arrastre
    • 2.5.8 Tamaño objetivo (mínimo): Tamaño mínimo del objetivo (≈ 44×44 px)

18. Cierre: que el foco sea una “luz amable”

  1. Orden de Tab = orden lógico, el primer Tab va al contenido principal.
  2. Mantén el anillo de foco grueso y de alto contraste; usa :focus-visible con criterio.
  3. Usa Roving TabIndex para unificar reglas dentro de UIs compuestas.
  4. Implementa siempre los cuatro del modal (abrir, trampa, Esc, restaurar).
  5. Atajos: decláralos, ofrece alternativas, evita conflictos—velocidad amable.
  6. Haz crecer la calidad con una prueba rápida de 5 minutos y reglas de design system.

Cuando el viaje del foco está bien iluminado, cualquier método de entrada conduce a la misma comprensión y resultado. Que tu UI sea una guía tranquila. Estoy aquí para ayudar, de corazón.

Salir de la versión móvil