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.

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)