Navegação por teclado em tabelas de dados que realmente vai para produção
Uma tabela de dados com quinhentas linhas e quinhentas paradas de Tab não é acessível por teclado — é hostil ao teclado. A navegação por teclado em tabelas de dados exige uma única parada de Tab, movimento entre células com setas e um modelo de foco visível que sobreviva à virtualização.
Usuários avançados vivem em tabelas de dados. Operadores editam registros em lote. Agentes de suporte escaneiam filas de tickets. Analistas percorrem grades financeiras. Todos usam o teclado — não eventualmente, não como caso marginal, mas como entrada principal em sessões de horas. A navegação por teclado em tabelas de dados não é uma caixa de acessibilidade aplicada depois que a tabela vai para produção. É infraestrutura da tabela: um ponto de entrada na sequência de tabulação, setas que movem um modelo de foco por célula e saídas de emergência que respeitam WCAG 2.1.2 ao não prender o usuário dentro da grade.
A maioria das implementações de tabelas de dados falha da mesma forma. Cada célula é focável. Tab percorre dez mil elementos. As setas rolam a página. Leitores de tela não anunciam nada útil porque a tabela é uma grade de div sem papéis. O componente parece uma planilha e se comporta como um labirinto.
Tabelas nativas e grades interativas são problemas distintos
Se a tabela é conteúdo tabular somente leitura com links ocasionais, uma <table> semântica com <th> com scope correto e links focáveis por teclado dentro das células pode bastar. Tab se move entre elementos interativos naturalmente. As setas não são necessárias porque a navegação célula a célula não é a interação principal.
Se a tabela é uma grade interativa — linhas selecionáveis, edições inline, colunas ordenáveis a partir das células, ações em massa sobre intervalos focados — o padrão grid do WAI-ARIA se aplica. A grade é um único widget composto: uma parada de Tab, navegação interna com setas, role="grid" no contêiner, role="row" e role="gridcell" nos descendentes (ou elementos de tabela nativos com semântica equivalente).
A árvore de decisão é direta:
- A interação principal é navegar célula a célula com setas → padrão de grade interativa.
- A interação principal é ler dados com cliques ocasionais em links → tabela nativa.
- As linhas se expandem hierarquicamente → padrão
treegrid, nãogridplano.
Escolher errado custa retrabalho. Adicionar navegação com setas a uma tabela de div que já colocou cada célula na sequência de tabulação exige remover valores de tabindex em toda a árvore de componentes.
Quinhentas linhas não deveriam significar quinhentas paradas de Tab.
Roving tabindex é o modelo de foco padrão
Roving tabindex gerencia o foco dentro de widgets compostos. Exatamente uma célula tem tabindex="0" a qualquer momento. Todas as outras têm tabindex="-1". As setas movem o 0 para a próxima célula, chamam .focus() nela e deixam a célula anterior em -1. Tab e Shift+Tab entram e saem da grade por completo — não visitam cada célula.
Inicialização:
- O contêiner recebe
role="grid"e um nome acessível viaaria-labelou referência labelled-by. - A primeira célula interativa ou navegável obtém
tabindex="0". - Todas as outras células obtêm
tabindex="-1". - O handler keydown no contêiner intercepta ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown — chama
preventDefault()para impedir o scroll da 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()
}Leitores de tela anunciam a posição quando as células incluem aria-rowindex, aria-colindex e aria-colcount / aria-rowcount na grade. Colunas ordenáveis expõem aria-sort nas células de cabeçalho. Linhas selecionadas usam aria-selected na linha ou célula conforme apropriado.
O foco deve ser visível. WCAG 2.4.7 exige um indicador de foco que os usuários possam ver. Componentes de célula personalizados que suprimem :focus-visible por razões estéticas falham para usuários de teclado e para a auditoria. O catálogo de capacidades do design system deveria documentar o anel de foco da grade como token obrigatório — não como variante de tema opcional.
Tabelas virtualizadas precisam de aria-activedescendant
Roving tabindex move o foco do DOM para cada célula. Isso funciona quando cada célula existe no DOM. Tabelas virtualizadas renderizam apenas as linhas visíveis — células fora da tela não existem, então .focus() não pode apontar para elas.
aria-activedescendant mantém o foco do DOM no contêiner da grade (tabindex="0") enquanto um atributo ponteiro referencia o ID da célula ativa. As setas atualizam o ponteiro e movem um anel de foco visual — CSS na classe de célula ativa, não o foco padrão do navegador, porque o foco do DOM permaneceu no contêiner.
Requisitos para activedescendant:
- O ID da célula referenciada deve existir no DOM quando ativa — a virtualização por scroll deve renderizar a linha que contém a célula ativa antes de atualizar o ponteiro.
- O estilo de foco visual é manual — o time é responsável pelo contraste e visibilidade do anel.
- O leitor de tela anuncia o descendente ativo quando o ponteiro muda — testar com NVDA, VoiceOver e JAWS; o comportamento varia.
Usar roving tabindex em tabelas pequenas e médias sem virtualização. Mudar para activedescendant quando linhas montam e desmontam durante o scroll, ou quando o profiling de performance mostra que mudanças de foco disparam re-renderizações custosas.
Armadilhas de teclado e ações em massa
WCAG 2.1.2 proíbe armadilhas de teclado. Uma grade que captura Tab indefinidamente falha — Tab deve sair para o próximo elemento da página. Escape pode limpar a seleção ou fechar editores inline, mas não deve deixar usuários sem saída.
Padrões comuns que funcionam:
- Shift+Seta estende o intervalo de seleção sem mover o modelo de foco separadamente.
- Espaço alterna a seleção de linha na linha focada.
- Enter ativa a ação principal da célula — abrir detalhe, iniciar edição inline.
- Escape cancela a edição inline e devolve o foco ao contêiner da grade.
Barras de ações em massa aparecem quando a seleção não está vazia. A barra é a próxima parada de Tab depois da grade — não inserida entre cada linha. Usuários de leitores de tela ouvem a contagem de seleção via região aria-live na barra quando a seleção muda.
Não vincular atalhos globais sem documentá-los e garantir que não entrem em conflito com comandos do leitor de tela ou padrões do navegador. Documentar atalhos em um painel de ajuda ? na página, acessível por teclado.
Como a navegação por teclado deveria funcionar em tabelas de dados de produção?
Essas perguntas detectam implementações que passam no QA visual e falham na auditoria de teclado.
Cada célula precisa estar na sequência de tabulação?
Não. Uma parada de Tab para a grade. Células recebem foco apenas via setas (roving tabindex) ou ponteiro activedescendant. Links e botões dentro das células podem receber foco quando a célula está ativa — Enter os ativa, ou Tab sai da grade para o próximo elemento da página em vez de visitar cada controle embutido.
Quando uma tabela de dados deveria usar role="grid" versus uma tabela nativa?
Usar grid quando a navegação célula a célula com setas é o modelo de interação principal — células editáveis, seleção estilo planilha, ordenação por teclado. Usar <table> nativa quando o conteúdo é documental e Tab-entre-links basta. Usar grid sem implementar navegação com setas é pior que uma tabela nativa — promete um comportamento que não existe.
Como testar a navegação por teclado antes do lançamento?
Passe manual: desconectar o mouse, completar três fluxos principais — ordenar, selecionar linhas, abrir detalhe da linha — usando apenas o teclado. Automatizado: axe-core para papéis ARIA; testes personalizados que simulam setas e verificam movimento de foco e valores de tabindex. Incluir um teste de fumaça com leitor de tela por release — NVDA no Windows ou VoiceOver no macOS — porque a correção ARIA e o comportamento de anúncio divergem.
Um argumento comum vai na direção oposta
A postura contrária sustenta que a navegação intensiva por teclado em tabelas é um nicho de usuários avançados — que mouse e toque cobrem noventa e cinco por cento dos usuários e que implementar roving tabindex é um mau retorno sobre investimento frente a outras funcionalidades.
Usuários de teclado incluem usuários avançados com o maior tempo de sessão e as maiores responsabilidades operacionais — os clientes que vivem oito horas por dia em painéis de administração. Regulamentações de acessibilidade no setor público e em contratação empresarial exigem cada vez mais conformidade WCAG como critério contratual. O argumento de ROI também ignora que grades com setas beneficiam usuários de mouse que clicam menos e navegam mais rápido uma vez aprendido o padrão.
O antipadrão é declarar acessibilidade sem testar as rotas de teclado — mais comum que a despriorização honesta e falha tanto para usuários quanto para auditorias.
Pontos-chave
- A navegação por teclado em tabelas de dados significa uma parada de Tab por grade, não uma por célula.
- Grades interativas usam
role="grid", roving tabindex e setas compreventDefaultno scroll. - Tabelas virtualizadas precisam de
aria-activedescendantcom foco visual manual e sincronização de scroll. - Tab e Shift+Tab devem sair da grade — WCAG 2.1.2 proíbe armadilhas de teclado.
- Indicadores de foco são obrigatórios — suprimir anéis de foco por estética falha WCAG 2.4.7.
- Testar com fluxos apenas-teclado e pelo menos um leitor de tela antes do lançamento.
Conclusão
Tabelas de dados estão entre os componentes mais usados e menos acessíveis do software empresarial. A lacuna não é a falta de conhecimento ARIA — é lançar grades onde cada célula compete por Tab e as setas rolam a página. A solução é arquitetural: escolher grade versus tabela nativa desde o início, implementar um modelo de foco de forma consistente e tratar as rotas de teclado como critérios de aceitação equivalentes ao design visual.
A lista de verificação para produção é curta: uma parada de Tab, as setas movem o foco, o foco é visível, Tab escapa, a virtualização sincroniza activedescendant. Tabelas que passam funcionam para os operadores que dependem delas. As que falham excluem esses usuários em silêncio — e eventualmente falharão também na revisão de contratação.
