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.

Design8 min de leitura
AcessibilidadeTabelas de dadosNavegação por tecladoWCAGPadrões UX
Compartilhar

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ão grid plano.

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:

  1. O contêiner recebe role="grid" e um nome acessível via aria-label ou referência labelled-by.
  2. A primeira célula interativa ou navegável obtém tabindex="0".
  3. Todas as outras células obtêm tabindex="-1".
  4. 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 com preventDefault no scroll.
  • Tabelas virtualizadas precisam de aria-activedescendant com 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.

Artigos relacionados

Paleta de comandos

Pesquise um comando para executar...