Navegación por teclado en tablas de datos que sí llega a producción
Una tabla de datos con quinientas filas y quinientas paradas de Tab no es accesible por teclado — es hostil al teclado. La navegación por teclado en tablas de datos requiere una sola parada de Tab, movimiento entre celdas con flechas y un modelo de foco visible que sobreviva a la virtualización.
Los usuarios avanzados viven en tablas de datos. Los operadores editan registros en lote. Los agentes de soporte escanean colas de tickets. Los analistas recorren grillas financieras. Todos usan el teclado — no eventualmente, no como caso marginal, sino como entrada principal en sesiones de horas. La navegación por teclado en tablas de datos no es una casilla de accesibilidad aplicada después de que la tabla sale a producción. Es infraestructura de la tabla: un punto de entrada en la secuencia de tabulación, flechas que mueven un modelo de foco por celda y salidas de emergencia que respetan WCAG 2.1.2 al no atrapar al usuario dentro de la grilla.
La mayoría de las implementaciones de tablas de datos fallan de la misma manera. Cada celda es enfocable. Tab recorre diez mil elementos. Las flechas desplazan la página. Los lectores de pantalla no anuncian nada útil porque la tabla es una grilla de div sin roles. El componente parece una hoja de cálculo y se comporta como un laberinto.
Las tablas nativas y las grillas interactivas son problemas distintos
Si la tabla es contenido tabular de solo lectura con enlaces ocasionales, una <table> semántica con <th> con scope correcto y enlaces enfocables por teclado dentro de las celdas puede bastar. Tab se mueve entre elementos interactivos de forma natural. Las flechas no son necesarias porque la navegación celda a celda no es la interacción principal.
Si la tabla es una grilla interactiva — filas seleccionables, ediciones en línea, columnas ordenables desde las celdas, acciones masivas sobre rangos enfocados — aplica el patrón grid de WAI-ARIA. La grilla es un único widget compuesto: una parada de Tab, navegación interna con flechas, role="grid" en el contenedor, role="row" y role="gridcell" en los descendientes (o elementos de tabla nativos con semántica equivalente).
El árbol de decisión es directo:
- La interacción principal es navegar celda a celda con flechas → patrón de grilla interactiva.
- La interacción principal es leer datos con clics ocasionales en enlaces → tabla nativa.
- Las filas se expanden jerárquicamente → patrón
treegrid, nogridplano.
Elegir mal cuesta retrabajo. Añadir navegación con flechas a una tabla de div que ya puso cada celda en la secuencia de tabulación exige arrancar valores de tabindex en todo el árbol de componentes.
Quinientas filas no deberían significar quinientas paradas de Tab.
Roving tabindex es el modelo de foco por defecto
Roving tabindex gestiona el foco dentro de widgets compuestos. Exactamente una celda tiene tabindex="0" en todo momento. Todas las demás tienen tabindex="-1". Las flechas mueven el 0 a la siguiente celda, llaman a .focus() sobre ella y dejan la celda anterior en -1. Tab y Shift+Tab entran y salen de la grilla por completo — no visitan cada celda.
Inicialización:
- El contenedor recibe
role="grid"y un nombre accesible víaaria-labelo referencia labelled-by. - La primera celda interactiva o navegable obtiene
tabindex="0". - Todas las demás celdas obtienen
tabindex="-1". - El manejador keydown en el contenedor intercepta ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown — llama a
preventDefault()para detener el scroll de la página.
function handleGridKeyDown(
e: React.KeyboardEvent,
rowIndex: number,
colIndex: number,
rowCount: number,
colCount: number
) {
const moves: Record<string, [number, number]> = {
ArrowRight: [0, 1],
ArrowLeft: [0, -1],
ArrowDown: [1, 0],
ArrowUp: [-1, 0],
}
const delta = moves[e.key]
if (!delta) return
e.preventDefault()
const [dr, dc] = delta
const nextRow = Math.max(0, Math.min(rowCount - 1, rowIndex + dr))
const nextCol = Math.max(0, Math.min(colCount - 1, colIndex + dc))
focusCell(nextRow, nextCol)
}
function focusCell(row: number, col: number) {
const prev = document.activeElement as HTMLElement | null
prev?.setAttribute("tabindex", "-1")
const next = document.querySelector(
`[data-row="${row}"][data-col="${col}"]`
) as HTMLElement
next?.setAttribute("tabindex", "0")
next?.focus()
}Los lectores de pantalla anuncian la posición cuando las celdas incluyen aria-rowindex, aria-colindex y aria-colcount / aria-rowcount en la grilla. Las columnas ordenables exponen aria-sort en las celdas de encabezado. Las filas seleccionadas usan aria-selected en la fila o celda según corresponda.
El foco debe ser visible. WCAG 2.4.7 exige un indicador de foco que los usuarios puedan ver. Los componentes de celda personalizados que suprimen :focus-visible por razones estéticas fallan a usuarios de teclado y a la auditoría. El catálogo de capacidades del design system debería documentar el anillo de foco de la grilla como token obligatorio — no como variante de tema opcional.
Las tablas virtualizadas necesitan aria-activedescendant
Roving tabindex mueve el foco del DOM a cada celda. Eso funciona cuando cada celda existe en el DOM. Las tablas virtualizadas renderizan solo las filas visibles — las celdas fuera de pantalla no existen, así que .focus() no puede apuntar a ellas.
aria-activedescendant mantiene el foco del DOM en el contenedor de la grilla (tabindex="0") mientras un atributo puntero referencia el ID de la celda activa. Las flechas actualizan el puntero y mueven un anillo de foco visual — CSS en la clase de celda activa, no el foco predeterminado del navegador, porque el foco del DOM permaneció en el contenedor.
Requisitos para activedescendant:
- El ID de la celda referenciada debe existir en el DOM cuando está activa — la virtualización por scroll debe renderizar la fila que contiene la celda activa antes de actualizar el puntero.
- El estilo de foco visual es manual — el equipo es responsable del contraste y la visibilidad del anillo.
- El lector de pantalla anuncia el descendiente activo cuando cambia el puntero — probar con NVDA, VoiceOver y JAWS; el comportamiento varía.
Usar roving tabindex en tablas pequeñas y medianas sin virtualización. Cambiar a activedescendant cuando las filas se montan y desmontan durante el scroll, o cuando el perfilado de rendimiento muestra que los cambios de foco disparan re-renderizados costosos.
Trampas de teclado y acciones masivas
WCAG 2.1.2 prohíbe las trampas de teclado. Una grilla que captura Tab indefinidamente falla — Tab debe salir al siguiente elemento de la página. Escape puede limpiar la selección o cerrar editores en línea, pero no debe dejar a los usuarios sin salida.
Patrones comunes que funcionan:
- Shift+Flecha extiende el rango de selección sin mover el modelo de foco por separado.
- Espacio alterna la selección de fila en la fila enfocada.
- Enter activa la acción principal de la celda — abrir detalle, iniciar edición en línea.
- Escape cancela la edición en línea y devuelve el foco al contenedor de la grilla.
Las barras de acciones masivas aparecen cuando la selección no está vacía. La barra es la siguiente parada de Tab después de la grilla — no se inserta entre cada fila. Los usuarios de lectores de pantalla escuchan el conteo de selección vía región aria-live en la barra cuando cambia la selección.
No vincular atajos globales sin documentarlos y asegurar que no entren en conflicto con comandos del lector de pantalla o valores predeterminados del navegador. Documentar atajos en un panel de ayuda ? en la página, accesible por teclado.
¿Cómo debería funcionar la navegación por teclado en tablas de datos de producción?
Estas preguntas detectan implementaciones que pasan el QA visual y fallan la auditoría de teclado.
¿Cada celda necesita estar en la secuencia de tabulación?
No. Una parada de Tab para la grilla. Las celdas reciben foco solo vía flechas (roving tabindex) o puntero activedescendant. Los enlaces y botones dentro de las celdas pueden recibir foco cuando la celda está activa — Enter los activa, o Tab sale de la grilla al siguiente elemento de la página en lugar de visitar cada control embebido.
¿Cuándo debería una tabla de datos usar role="grid" frente a una tabla nativa?
Usar grid cuando la navegación celda a celda con flechas es el modelo de interacción principal — celdas editables, selección estilo hoja de cálculo, ordenamiento por teclado. Usar <table> nativa cuando el contenido es documental y Tab-entre-enlaces basta. Usar grid sin implementar navegación con flechas es peor que una tabla nativa — promete un comportamiento que no existe.
¿Cómo probar la navegación por teclado antes del lanzamiento?
Pase manual: desconectar el mouse, completar tres flujos principales — ordenar, seleccionar filas, abrir detalle de fila — usando solo el teclado. Automatizado: axe-core para roles ARIA; pruebas personalizadas que simulan flechas y verifican movimiento de foco y valores de tabindex. Incluir una prueba de humo con lector de pantalla por release — NVDA en Windows o VoiceOver en macOS — porque la corrección ARIA y el comportamiento de anuncio divergen.
Un argumento habitual va en la dirección opuesta
La postura contraria sostiene que la navegación intensiva por teclado en tablas es un nicho de usuarios avanzados — que el mouse y el tacto cubren el noventa y cinco por ciento de los usuarios y que implementar roving tabindex es un mal retorno de inversión frente a otras funcionalidades.
Los usuarios de teclado incluyen usuarios avanzados con el mayor tiempo de sesión y las mayores responsabilidades operativas — los clientes que viven ocho horas al día en paneles de administración. Las regulaciones de accesibilidad en el sector público y la contratación empresarial exigen cada vez más conformidad WCAG como criterio contractual. El argumento de ROI también ignora que las grillas con flechas benefician a usuarios de mouse que hacen menos clics y navegan más rápido una vez aprendido el patrón.
El antipatrón es declarar accesibilidad sin probar las rutas de teclado — más común que la depriorización honesta y falla tanto a usuarios como a auditorías.
Puntos clave
- La navegación por teclado en tablas de datos significa una parada de Tab por grilla, no una por celda.
- Las grillas interactivas usan
role="grid", roving tabindex y flechas conpreventDefaulten el scroll. - Las tablas virtualizadas necesitan
aria-activedescendantcon foco visual manual y sincronización de scroll. - Tab y Shift+Tab deben salir de la grilla — WCAG 2.1.2 prohíbe las trampas de teclado.
- Los indicadores de foco son obligatorios — suprimir anillos de foco por estética falla WCAG 2.4.7.
- Probar con flujos solo-teclado y al menos un lector de pantalla antes del lanzamiento.
Conclusión
Las tablas de datos están entre los componentes más usados y menos accesibles del software empresarial. La brecha no es la falta de conocimiento ARIA — es lanzar grillas donde cada celda compite por Tab y las flechas desplazan la página. La solución es arquitectónica: elegir grilla frente a tabla nativa desde el inicio, implementar un modelo de foco de forma consistente y tratar las rutas de teclado como criterios de aceptación equivalentes al diseño visual.
La lista de verificación para producción es corta: una parada de Tab, las flechas mueven el foco, el foco es visible, Tab escapa, la virtualización sincroniza activedescendant. Las tablas que pasan funcionan para los operadores que dependen de ellas. Las que fallan excluyen a esos usuarios en silencio — y eventualmente fallarán también la revisión de contratación.
