Skip to content

pesquisa componente table

Mateus Villain edited this page Aug 22, 2026 · 1 revision

Pesquisa de componente: Table

Premissa de escopo: este documento trata do componente Table / Data Table — a tabela de dados usada para exibir, comparar e (opcionalmente) manipular conjuntos de dados estruturados em produtos de software. Cobre tanto a variante estática (role table do WAI-ARIA, apenas leitura) quanto a variante interativa (role grid/treegrid, com navegação por célula, edição e seleção), já que praticamente todo design system maduro oferece as duas — e a escolha entre elas é, em si, uma das decisões de arquitetura mais importantes do componente. Não cobre Tree isolado (hierarquia sem tabela) nem List/Resource list (itens homogêneos sem colunas comparáveis), que são tratados como alternativas na seção 1.


1. Definição e propósito

Uma Table é um componente que organiza um conjunto de dados em uma grade bidimensional de linhas e colunas, onde cada coluna representa um atributo (campo) e cada linha representa um registro (item), permitindo comparação direta entre itens ao longo dos mesmos atributos.

O problema que resolve é comparação estruturada: quando o usuário precisa escanear muitos itens do mesmo tipo e comparar valores específicos entre eles (preço, status, data, responsável), nenhuma outra estrutura visual comunica isso tão eficientemente quanto linhas e colunas alinhadas. O objetivo principal não é "mostrar uma lista de coisas" — é permitir varredura e comparação rápidas de múltiplos atributos por múltiplos itens simultaneamente.

Deve ser usado quando:

  • Os itens compartilham os mesmos atributos (schema homogêneo) e o usuário precisa comparar esses atributos entre itens (ex.: lista de pedidos com data, cliente, valor, status).
  • Há necessidade de ordenação, filtragem ou agregação por coluna.
  • O volume de dados é grande o suficiente para que paginação, ordenação e escaneio vertical sejam relevantes.

Não deve ser usado quando:

  • Os itens têm atributos heterogêneos ou uma quantidade de metadados que não cabe em colunas legíveis (ex.: cards de produto com imagem, descrição longa e badges variados) — nesse caso um Resource list / Card grid comunica melhor.
  • Há relação hierárquica forte entre os itens (pastas e arquivos, comentários aninhados) sem necessidade de comparar colunas — um Tree view é mais adequado; se a hierarquia e as colunas importam ao mesmo tempo, usa-se Treegrid.
  • O conteúdo é essencialmente uma lista de ações/links homogêneos sem múltiplos atributos comparáveis — uma List simples é mais leve e não exige toda a semântica tabular.
  • A tela é muito estreita (mobile) e as colunas essenciais não cabem — nesse caso a alternativa costuma ser transformar cada linha em um card empilhado (ver seção 10).

Componentes alternativos e quando preferi-los:

Alternativa Prefira quando
List / Resource list Itens homogêneos, poucos atributos, foco em navegação (clicar para abrir), não em comparação de colunas.
Tree view Hierarquia é o dado principal; não há colunas para comparar.
Treegrid Hierarquia e colunas comparáveis coexistem (ex.: estrutura de pastas com tamanho e data de modificação).
Card grid Conteúdo visualmente rico (imagens, múltiplas linhas de texto) que não cabe em células de largura fixa.
Description list (key-value) Um único item com muitos atributos (não comparação entre itens, e sim detalhamento de um item).

Qualidade da decisão: o "trabalho a ser feito" pela Table é sempre comparar N itens ao longo dos mesmos M atributos. Se o produto não precisa que o usuário compare itens entre si, provavelmente não precisa de uma tabela — precisa de uma lista.


2. Modelo mental do usuário

Usuários trazem para qualquer tabela digital as mesmas expectativas de uma planilha ou de uma tabela impressa, reforçadas por décadas de Excel, Google Sheets e páginas HTML <table>:

  • Cabeçalho fixo no topo, alinhado com a coluna correspondente. Qualquer desalinhamento entre header e dado quebra a leitura instantaneamente.
  • Clicar no cabeçalho ordena a coluna. É uma convenção tão forte (reforçada por toda ferramenta de e-mail, planilha e admin de e-commerce) que um cabeçalho clicável que não ordena, ou uma coluna ordenável cujo cabeçalho não parece clicável, gera frustração imediata.
  • Números alinham à direita, texto à esquerda. Isso vem diretamente do hábito de leitura de valores monetários e numéricos em planilhas — dígitos alinhados nas casas decimais facilitam comparação vertical.
  • Selecionar linhas via checkbox na primeira coluna, com "selecionar tudo" no cabeçalho. Convenção estabelecida por clientes de e-mail (Gmail) e admins de e-commerce.
  • Zebra striping ou linhas divisórias para "não perder a linha" ao ler horizontalmente. Em tabelas largas, o usuário depende de uma pista visual para não pular de linha ao mover o olhar da esquerda para a direita.
  • Rolagem: cabeçalho "gruda" (sticky) ao rolar verticalmente, e a primeira coluna (geralmente identificadora) "gruda" ao rolar horizontalmente — comportamento copiado diretamente do congelamento de painéis do Excel.

Relação com plataformas nativas: não existe um "table nativo" de sistema operacional no mesmo sentido que existe um date picker nativo — a tabela sempre foi um componente de aplicação (desktop ou web). Ainda assim, o modelo mental do Finder (macOS) e do Explorer (Windows) em modo "detalhes" — colunas ordenáveis, redimensionáveis, com ícone de seta indicando direção — é a referência inconsciente mais próxima de "tabela de sistema" que a maioria dos usuários carrega.

Custo de quebrar a convenção: uma tabela sem indicação visual de coluna ordenada, ou com ordenação que não persiste ao recarregar, gera desconfiança sobre se a ação "funcionou". Vale quebrar a convenção (ex.: ordenação sempre desabilitada) apenas quando o dado é claramente não-ordenável e isso é comunicado com clareza (ausência total do affordance de sort), nunca com um cabeçalho ambíguo.


3. Anatomia

Descrição de fora para dentro:

Elemento Tipo Função
Container Auxiliar Envolve toda a tabela; define borda, cantos arredondados, sombra, scroll horizontal quando a tabela excede a largura disponível.
Toolbar / cabeçalho da tabela Opcional Título, descrição, busca, filtros, ações em lote, botão de gerenciar colunas. Fica fora do <table> semanticamente.
Header row (thead) Obrigatório Linha fixa com os nomes das colunas.
Header cell (th) Obrigatório Nome da coluna; pode conter botão de ordenação, tooltip de definição, alça de redimensionamento.
Checkbox de "selecionar tudo" Opcional Fica na primeira header cell quando há seleção em lote.
Body (tbody) Obrigatório Contém as linhas de dados.
Row (tr) Obrigatório Um registro; pode ser clicável/navegável, selecionável, expansível.
Cell (td) Obrigatório Um valor de um atributo para aquele registro. Alinhamento depende do tipo de dado.
Checkbox de seleção de linha Opcional Primeira célula da linha, quando há seleção.
Row header Auxiliar Célula (geralmente a primeira) que identifica a linha para leitores de tela — nem sempre visualmente distinta, mas semanticamente crítica.
Célula de ação (overflow menu, botões) Opcional Geralmente a última coluna; ações contextuais por linha.
Ícone/indicador de ordenação Opcional Seta ou chevron no header cell da coluna ordenada.
Linha de resumo/totais (footer/tfoot) Opcional Agregações por coluna (soma, média).
Estado vazio (empty state) Auxiliar Substitui o corpo quando não há dados a exibir.
Skeleton/loading rows Auxiliar Placeholder animado enquanto os dados carregam.
Paginação Auxiliar (externo ou integrado) Controles para navegar entre páginas de dados; tratada por muitos sistemas como componente separado composto com a tabela.
Linha expansível (disclosure) Opcional Conteúdo adicional revelado abaixo da linha principal.
Sub-header / linha de agrupamento Opcional Agrupa um conjunto de linhas sob um cabeçalho secundário (ex.: Polaris IndexTable subheaders).

Hierarquia visual: container → (toolbar) → header row → body rows → (footer) → (paginação). Dentro de cada linha, a ordem de leitura é esquerda→direita (ou direita→esquerda em RTL), com a primeira coluna geralmente carregando a maior carga identificadora (nome, ID) e por isso frequentemente fixada (sticky) durante rolagem horizontal.


4. Estados

Estado Quando ocorre Comportamento Representação visual
Default Linha/célula sem interação Conteúdo estático Cor de texto padrão, sem fundo diferenciado
Hover (linha) Ponteiro sobre a linha Sinaliza que a linha é escaneável/acionável Fundo levemente destacado em toda a extensão da linha
Hover (header sortable) Ponteiro sobre cabeçalho ordenável Sinaliza affordance de clique Ícone de ordenação aparece/destaca; cursor pointer
Focus Elemento recebeu foco programático (ex.: após ação) Nem sempre visível — depende do meio de entrada Anel de foco aplicado, mas pode ser suprimido por CSS
Focus-visible Foco via teclado (Tab/setas) Deve sempre ser visível para navegação por teclado Anel de foco de alto contraste ao redor da célula/linha/controle focado
Selected Checkbox da linha marcado Linha é incluída em ações em lote Fundo da linha muda (geralmente tom de cor primária/secundária), checkbox marcado
Indeterminate (checkbox "todos") Seleção parcial (algumas linhas, não todas) O checkbox do cabeçalho reflete seleção parcial Ícone de traço (—) em vez de check
Sorted (asc/desc) Coluna está sendo usada para ordenar Dados reordenados; header comunica direção Ícone de seta para cima/baixo no header cell; aria-sort
Sortable (não ativo) Coluna pode ser ordenada mas não está no momento Nenhuma reordenação aplicada Ícone de ordenação neutro, geralmente só visível no hover/focus
Disabled (linha) Linha existe mas não pode ser selecionada/acionada Ignora cliques e não recebe foco por padrão (ou recebe foco sem ativar, conforme a política de foco adotada) Texto e controles com opacidade reduzida
Loading (tabela inteira) Requisição de dados em andamento Bloqueia interações que dependem do dataset (ordenar, paginar) Skeleton rows ou spinner sobreposto
Loading (linha individual) Ação assíncrona em uma linha (ex.: exclusão) Linha fica temporariamente não interativa Spinner na célula de ação, opacidade reduzida na linha
Empty Dataset vazio (sem itens ou filtro sem resultado) Corpo da tabela substituído por mensagem/CTA Ilustração ou texto centralizado no lugar das linhas
Error Falha ao carregar dados Impede exibição de dados parciais/incorretos Mensagem de erro com ação de retry no lugar do corpo
Expanded/Collapsed Linha com disclosure (detalhes) Mostra/oculta conteúdo adicional abaixo da linha Chevron rotaciona; aria-expanded alternado
Editing (célula) Tabela editável, célula ativada para edição Substitui exibição estática por input Célula ganha borda de campo de formulário
Sticky (header/coluna) Rolagem ultrapassa o header ou a primeira coluna Elemento permanece fixo visualmente Sombra sutil separando a área fixa do conteúdo rolado

Diferenciação crítica: focus ≠ focus-visible — em uma tabela interativa (grid), o foco se move célula a célula via teclado com muita frequência; se todo movimento de foco disparasse um anel visível também em interação por mouse/toque, a UI ficaria "ruidosa". Use :focus-visible para restringir o anel a navegação por teclado. Disabled ≠ readonly: uma linha disabled normalmente sai do fluxo de interação (não deveria ser focável por padrão), enquanto uma célula readonly permanece focável e legível por leitor de tela, apenas não aceita edição — distinção importante em tabelas editáveis.


5. Interações

Mouse:

  • Clique em header cell ordenável → ordena/inverte ordenação da coluna; um único clique de cada vez, sem exigir modificadores.
  • Clique na linha (quando a linha inteira é navegável) → abre detalhe; conflita com seleção por checkbox, então a área clicável de navegação deve ser distinta da área do checkbox (evitar que marcar a seleção também dispare navegação).
  • Duplo clique (em tabelas editáveis) → ativa edição inline da célula.
  • Hover → realça a linha inteira para facilitar leitura horizontal; em headers, revela affordance de ordenação/redimensionamento.
  • Drag na borda do header cell → redimensiona coluna.
  • Drag da linha (com handle dedicado) → reordena linhas, quando suportado.

Touch:

  • Tap → equivalente ao clique (seleção, navegação, ordenação).
  • Tap-and-hold (long press) → em alguns padrões mobile, ativa modo de seleção múltipla sem precisar tocar diretamente no checkbox.
  • Swipe na linha → revela ações contextuais (padrão comum em listas mobile que substituem tabelas em telas estreitas).
  • Scroll horizontal por arraste → navega colunas que excedem a viewport, já que hover para redimensionar não existe em touch.

Teclado (fundamental distinguir tabela estática de interativa, ver seção 6):

Tabela estática (role table): não é um widget — não há navegação especial por célula. O Tab passa pelos elementos focáveis dentro das células (links, botões) na ordem do DOM, como em qualquer conteúdo de página.

Tabela interativa (role grid/treegrid, padrão Data Grid do APG):

  • Tab / Shift+Tab: entra e sai do grid como um todo (um único parada no grid, não uma parada por célula) — dentro do grid, o Tab move o foco para fora do grid ou para o próximo widget focável se a implementação usar aria-activedescendant.
  • Setas (↑↓←→): movem o foco célula a célula dentro do grid.
  • Ctrl+Home / Ctrl+End: move para a primeira/última célula do grid.
  • Home / End: move para o início/fim da linha atual.
  • Page Up / Page Down: rola e move o foco por "página" de linhas visíveis.
  • Enter / F2 (em grids editáveis): ativa o modo de edição da célula focada.
  • Esc: cancela a edição em andamento, retornando ao modo de navegação.
  • Space: em uma célula com checkbox, alterna a seleção da linha.
  • Shift + clique/Space: seleciona um intervalo de linhas (do último item selecionado até o atual).
  • Ctrl/Cmd + clique: alterna seleção individual sem afetar as demais.

Qualidade: o teclado é o ponto mais frequentemente esquecido em tabelas customizadas, e é exatamente onde a diferença entre table e grid importa mais — implementar navegação por setas em uma tabela que só precisava do papel semântico table é trabalho desnecessário e, pior, pode confundir usuários de leitor de tela que esperam comportamento de tabela estática. Implemente grid apenas quando há de fato interatividade célula a célula (edição, seleção fina, ações por célula).


6. Acessibilidade

Baseado no WAI-ARIA Authoring Practices Guide (APG), especificamente nos padrões Table e Grid, além das Grid and Table Properties.

Semântica HTML

A primeira decisão de acessibilidade — e a mais consequente — é: table estático ou grid interativo? O próprio APG define isso explicitamente: "Como um elemento HTML table, uma tabela WAI-ARIA é uma estrutura tabular estática contendo uma ou mais linhas que cada uma contém uma ou mais células; não é um widget interativo." Já o grid é usado quando há navegação por célula, edição de conteúdo, ou controles interativos organizados espacialmente (não apenas um link ou botão dentro de uma célula ocasional).

Regra prática: se as células contêm apenas texto ou, no máximo, um link/botão isolado navegável via Tab normal, use HTML <table> nativo (a primeira regra do ARIA — "não use ARIA quando HTML nativo resolve"). Só promova para role="grid" (ou use <table> com atributos ARIA de grid sobrepostos) quando o usuário precisa navegar pelas células com as setas, geralmente porque há edição inline, múltiplos controles por célula, ou seleção granular que exige foco preciso.

ARIA — roles, states e properties

Para tabela estática (HTML nativo, preferencial):

  • <table>, <caption>, <thead>, <tbody>, <tfoot>, <tr>, <th scope="col">, <th scope="row"> (quando a primeira coluna identifica a linha), <td>.
  • aria-sort="ascending" | "descending" | "none" | "other" no <th> da coluna atualmente ordenada.
  • aria-describedby no <table> apontando para um elemento com descrição/legenda adicional, quando a <caption> não é suficiente.
  • aria-rowcount e aria-rowindex quando o DOM não contém todas as linhas disponíveis (ex.: virtualização) — comunica ao leitor de tela quantas linhas existem no total além das renderizadas.
  • aria-colcount e aria-colindex para o equivalente em colunas (ex.: colunas ocultas/virtualizadas).

Para grid interativo:

  • role="grid" no container (ou role="treegrid" para hierarquia), role="row", role="gridcell" (ou role="columnheader"/role="rowheader" conforme aplicável).
  • aria-multiselectable="true" quando múltiplas linhas podem ser selecionadas.
  • aria-selected="true|false" em cada linha/célula selecionável.
  • aria-readonly="true" em células ou no grid inteiro quando a edição está temporariamente desabilitada.
  • aria-activedescendant ou roving tabindex (tabindex="0" na célula ativa, tabindex="-1" nas demais) para gerenciar foco — o APG recomenda roving tabindex quando possível, pois o navegador rola o elemento focado para a viewport automaticamente, o que não acontece com aria-activedescendant.
  • aria-expanded em linhas expansíveis (disclosure) ou em nós de treegrid.
  • aria-level, aria-posinset, aria-setsize em treegrid, para comunicar profundidade e posição na hierarquia.

Navegação por teclado e gerenciamento de foco

Já detalhado na seção 5. O ponto crítico de acessibilidade é a consistência do padrão de foco único (single tab stop): o grid inteiro é um único parada no Tab da página; a navegação interna é feita por setas. Isso segue o mesmo princípio de "roving tabindex" usado em outros widgets compostos (toolbar, tabs, tree) — documentado na prática geral de Developing a Keyboard Interface do APG.

Leitores de tela

  • Cada <th> deve ter scope correto (col ou row) para que o leitor de tela anuncie automaticamente "Nome da coluna: valor" ao navegar pelas células — essa é a razão estrutural mais forte para preferir HTML nativo a uma <div> estilizada como tabela.
  • Mudanças de ordenação devem ser comunicadas: o aria-sort já cobre a maior parte disso ao ser lido junto ao cabeçalho; para reforço, uma região aria-live="polite" pode anunciar "Tabela ordenada por Data, decrescente" após a ação, especialmente relevante em SPAs onde a mudança visual pode não ser percebida pelo AT.
  • Seleção de linha: ao marcar/desmarcar, aria-selected já comunica o estado; evite depender apenas de cor de fundo.
  • Em grids editáveis, a transição para o modo de edição deve mover o foco para o campo de input e o Esc deve devolver o foco à célula (não ao topo da página).

Outros (WCAG 2.2)

  • Contraste: texto de dados e cabeçalhos devem atender 4.5:1 (texto normal) — atenção especial a estados "disabled" ou "muted" de linhas, que tendem a violar contraste mínimo se usados apenas para hierarquia visual.
  • Área mínima de toque: controles dentro de células (checkbox, botão de ação, ícone de ordenação) devem ter no mínimo 24×24px CSS (WCAG 2.2 SC 2.5.8), idealmente 44×44px — desafio real em tabelas densas, onde células compactas competem por espaço com área de toque adequada; a solução comum é aumentar a área de toque com padding invisível sem aumentar o tamanho visual do ícone.
  • Zoom/reflow (SC 1.4.10): a 400% de zoom, uma tabela larga não deve forçar rolagem horizontal da página inteira — apenas do container da tabela, mantendo o restante da UI acessível. Isso reforça por que a rolagem horizontal deve ser isolada ao componente (container com overflow-x), não vazada para o <body>.
  • Necessidades cognitivas/motoras: tabelas muito densas com muitas ações por linha aumentam carga cognitiva; agrupar ações secundárias em um menu overflow (⋮) reduz ruído visual sem eliminar funcionalidade.

7. Validação e feedback

Aplica-se apenas ao modo de edição inline (tabela editável) — para a tabela puramente de leitura/seleção, esta seção não se aplica em sua forma clássica de "formulário".

  • Quando validar: idealmente on blur da célula (ao sair do campo de edição) ou on submit de uma linha inteira, nunca on change a cada tecla — validação por tecla em uma grade densa gera ruído visual excessivo e interrompe o fluxo de digitação.
  • Comunicação de erro: a célula com erro deve usar aria-invalid="true" e aria-describedby apontando para uma mensagem breve, visível preferencialmente como um popover/tooltip ancorado à célula (não um bloco que empurra o layout da tabela, o que causaria reflow indesejado em outras linhas).
  • Trade-off do timing: validar cada célula isoladamente (granular) é mais amigável para edição rápida, mas pode mascarar inconsistências entre colunas relacionadas (ex.: data de início > data de fim); nesses casos, uma validação adicional "de linha" no momento de salvar é necessária.
  • Preservar o que o usuário digitou: se o salvamento falhar (erro de rede), a célula deve manter o valor editado e sinalizar erro, nunca reverter silenciosamente para o valor anterior — perder digitação em uma tabela grande é especialmente frustrante porque o usuário pode ter editado múltiplas células.
  • Relaciona-se com a seção 4 (estados error/editing) e a seção 6 (aria-invalid, aria-describedby).

8. Conteúdo e UX Writing

  • Cabeçalhos de coluna: curtos (idealmente 1–2 palavras), em title case ou sentence case conforme o guia de voz do produto, mas consistentes entre colunas. Evite abreviações obscuras; se o espaço for restrito, trunque com reticências e use title/tooltip para o texto completo, nunca abrevie de forma ambígua.
    • ✅ "Valor total" / ❌ "V. Tot."
  • Células vazias: represente ausência de dado de forma explícita e não confundível com zero ou erro.
    • ✅ "—" ou "Não informado" / ❌ deixar a célula completamente em branco (o usuário não sabe se é um bug de carregamento ou um dado ausente)
  • Mensagens de estado vazio (tabela sem dados): explique o motivo e ofereça uma ação, distinguindo "nunca houve dados" de "filtro não retornou resultados".
    • ✅ "Nenhum pedido encontrado para os filtros aplicados. [Limpar filtros]" / ❌ "Nenhum resultado"
  • Mensagens de erro de carregamento: acionáveis, sem jargão técnico.
    • ✅ "Não foi possível carregar os dados. [Tentar novamente]" / ❌ "Erro 500"
  • Labels de ação em massa: verbo + quantidade, para reforçar o escopo da ação.
    • ✅ "Excluir 3 itens selecionados" / ❌ "Excluir"
  • Tooltips de cabeçalho: usados apenas quando o nome da coluna sozinho é ambíguo (ex.: "Taxa" pode significar taxa de conversão ou taxa cobrada) — não como muleta para nomes de coluna mal escolhidos.

9. Variantes

Variante Quando usar Quando evitar / é realmente necessária?
Estática (table) Leitura, comparação, sem edição/seleção granular Padrão-base; deveria ser o default do sistema
Interativa (grid) Edição inline, navegação por célula, ações complexas por célula Só se a interação célula-a-célula for real; caso contrário é complexidade desnecessária
Com seleção (checkbox) Ações em lote sobre múltiplos itens Evitar se a tabela nunca oferece ação em lote — checkbox "decorativo" gera expectativa falsa
Com ordenação Dataset onde ordenar por atributo agrega valor real (datas, valores, status) Colunas de texto livre longo raramente precisam de ordenação alfabética útil
Densa / compacta Grandes volumes, usuários avançados (admin, dashboards internos) Prejudica área de toque em contexto touch — evitar como default em produtos mobile-first
Confortável (padrão) Uso geral —
Com zebra striping Tabelas muito largas ou com muitas linhas, para ajudar a "não perder a linha" Redundante em tabelas estreitas/poucas linhas; pode até prejudicar contraste de badges/status coloridos
Com bordas (grid lines) Dados muito densos, colunas numéricas que precisam de separação clara Visualmente mais pesada; a maioria dos sistemas modernos prefere divisórias sutis ou espaçamento
Expansível (disclosure rows) Detalhe secundário que não cabe/não precisa estar sempre visível Evitar se o conteúdo expandido for crítico para a decisão do usuário — force visibilidade em vez de escondê-lo
Com paginação Datasets grandes carregados em blocos —
Com scroll infinito / virtualização Datasets muito grandes onde paginação numérica atrapalha o fluxo (ex.: feeds) Mais difícil de tornar acessível (ver aria-rowcount); evitar se paginação simples resolve
Sticky header / sticky column Tabelas altas ou largas onde o contexto (nome da coluna/linha) se perde ao rolar Custo de implementação e possíveis bugs de z-index; avaliar se a tabela é realmente grande o suficiente para justificar

Combate à proliferação: "densa" e "confortável" deveriam ser a única variante de densidade (token de espaçamento, não componente separado); "com seleção", "com ordenação" e "expansível" são capacidades opcionais habilitadas via props, não variantes visuais distintas — a API (seção 12) deve refletir isso como booleans/composição, não como um enum fechado de "tipos de tabela".


10. Responsividade

Desktop: layout completo em colunas, hover states ativos, redimensionamento de coluna via mouse, tooltips no hover.

Tablet: geralmente mantém o layout de tabela, mas com maior densidade de toque (áreas clicáveis maiores que no desktop) e menos dependência de hover — ações que só apareciam no hover (ex.: ícone de editar) precisam ficar sempre visíveis ou acessíveis via tap.

Mobile: a transformação mais comum é a tabela virar uma lista de cards empilhados, onde cada linha original se torna um bloco vertical com pares "label: valor" (um pequeno description list por item) — abordagem adotada, por exemplo, pelos web components mais recentes do Polaris (<s-table>), que "renderizam como listas em telas pequenas e como tabelas em telas maiores". Alternativas incluem:

  • Manter a tabela e permitir scroll horizontal, fixando apenas a primeira coluna (identificador) — funciona bem quando há poucas colunas essenciais e o usuário já entende que "existe mais para o lado".
  • Reduzir colunas visíveis por padrão, com um controle de "gerenciar colunas" para escolher quais aparecem.

Por que a transformação ocorre: colunas fixas em pixels ou porcentagem simplesmente não cabem em uma tela de ~360–420px quando há mais de 3–4 atributos; forçar a manutenção do layout tabular gera texto ilegível ou scroll horizontal excessivo, prejudicando a varredura vertical que é o ponto forte de uma tabela. A troca para cards prioriza legibilidade em detrimento da comparação lado a lado — um trade-off aceitável, já que em telas pequenas o usuário tende a olhar item por item, não comparar N itens simultaneamente.

Área de toque e densidade: em qualquer formato mobile, os alvos interativos (checkbox, botão de ação, chevron de expandir) precisam respeitar o mínimo de toque mesmo que isso obrigue a aumentar a altura da linha/card além do "confortável" desktop.


11. Dependências e composição

  • Depende de / usa internamente: Checkbox (seleção), Button/IconButton (ações, ordenação), Menu/Dropdown (ações em massa, overflow menu por linha), Tooltip (definições de coluna, truncamento), Badge/Tag (indicadores de status em célula), Skeleton (loading), Empty state, Pagination, Spinner, Avatar (frequentemente em colunas de "responsável"/"usuário").
  • Compõe-se com: Toolbar/Filter bar (busca, filtros, exportação), Card (tabela embutida em um card com título), Tabs (múltiplas visões do mesmo dataset), Modal/Drawer (edição detalhada de um item selecionado a partir da tabela).
  • Relação com outros componentes do design system: Table compartilha padrões de foco/seleção com Tree e List (todos usam roving tabindex e aria-selected); compartilha padrões de ordenação e paginação com qualquer outra visualização de dataset.

Classificação: Table é um componente composto — não é um átomo (depende de Checkbox, Button, Badge etc. para funcionar plenamente) nem um simples padrão de composição arbitrária (tem uma estrutura interna fixa e regras de acessibilidade rígidas que o distinguem de, por exemplo, um layout genérico em grid CSS). Quando combinada com toolbar, filtros e paginação em um bloco reutilizável, a composição resultante ("Data table completo") se aproxima de um padrão (pattern) de nível de página.


12. API e implementação

Proposta de API para uma implementação React/TypeScript, priorizando controlado por padrão (o estado de ordenação/seleção vive no consumidor) com opção de modo não-controlado para casos simples — decisão justificada porque tabelas quase sempre precisam sincronizar estado com fetch de dados no backend (paginação, ordenação server-side), o que exige que o consumidor tenha acesso ao estado.

<Table
  ariaLabel="Lista de pedidos"
  columns={columns}          // Column<T>[]
  data={rows}                // T[]
  rowKey={(row) => row.id}   // identificador estável por linha

  // Seleção
  selectable="multiple"      // 'none' | 'single' | 'multiple'
  selectedRowKeys={selected}
  onSelectionChange={setSelected}

  // Ordenação
  sortState={{ columnKey: 'createdAt', direction: 'desc' }}
  onSortChange={handleSort}

  // Estados
  isLoading={isLoading}
  error={error}
  emptyState={<EmptyState title="Nenhum pedido encontrado" />}

  // Densidade e aparência
  density="comfortable"      // 'compact' | 'comfortable'
  striped={false}
  stickyHeader
  stickyFirstColumn

  // Interatividade avançada (opcional — promove para role="grid")
  interactive={false}        // habilita navegação por célula/edição
  onRowAction={(row) => openDetail(row)}
/>
interface Column<T> {
  key: string;
  header: string;
  accessor: (row: T) => React.ReactNode;
  sortable?: boolean;
  align?: 'start' | 'end';     // não 'left'/'right' — pensa em RTL
  width?: number | string;
  isRowHeader?: boolean;        // marca a coluna que identifica a linha para AT
}

Decisões de API comentadas:

  • align: 'start' | 'end' em vez de 'left' | 'right': suporte nativo a RTL sem lógica condicional no consumidor.
  • selectable como enum, não boolean: 'none' | 'single' | 'multiple' cobre o caso real de seleção única (ex.: escolher uma linha para uma ação) sem forçar o consumidor a simular isso com multiple e lógica extra.
  • interactive como boolean explícito: força o time a decidir conscientemente entre role="table" (default, mais simples e mais seguro para AT) e role="grid" — evitando que a complexidade de grid seja aplicada "por via das dúvidas".
  • isRowHeader na definição da coluna, não um prop separado no Row: mantém a decisão de acessibilidade junto à definição do schema da tabela, coerente com a variante 9 ("com seleção"/"com ordenação" como capacidades, não variantes visuais).
  • Sem prop de paginação embutida obrigatória: paginação é tratada como componente composto externamente (<Table> + <Pagination>), já que datasets pequenos não precisam da complexidade, e o estado de paginação frequentemente vive no roteamento/URL do consumidor — reflete a decisão observada em vários sistemas de mercado (seção 15) de tratar paginação como componente à parte.

13. Casos extremos e edge cases

Caso Comportamento esperado
Conteúdo de célula muito longo Truncar com reticências e title/tooltip com o texto completo; nunca deixar quebrar o layout da coluna vizinha. Para nomes/títulos onde truncar prejudica a distinção entre itens (ex.: "Navy Merino Wool Blazer com..." vs "Navy Merino Wool Blazer sem..."), preferir wrap (quebra de linha) a truncamento, como recomenda o Polaris.
Ausência total de dados (dataset vazio) Estado vazio dedicado (seção 8), nunca uma tabela "fantasma" com apenas o header.
Filtro sem resultados Estado vazio diferente do dataset vazio — deve mencionar os filtros aplicados e oferecer "limpar filtros".
Muitas colunas (tabela mais larga que a viewport) Scroll horizontal isolado ao container, primeira coluna sticky; nunca forçar scroll horizontal da página inteira.
Poucas colunas em tela larga Evitar que colunas se estiquem desproporcionalmente (texto "boiando" no meio de um espaço enorme); definir largura máxima por coluna ou alinhar o conjunto à esquerda.
Erro de integração / falha ao carregar Estado de erro dedicado com ação de retry; nunca renderizar uma tabela parcialmente populada como se fosse sucesso.
Disabled + Loading simultâneos (ação em linha) Loading tem precedência visual (spinner substitui o controle), mas a linha continua semanticamente disabled para novas interações até resolver.
Item selecionado é removido do dataset (ex.: outro usuário excluiu) Remover da lista de selecionados silenciosamente e atualizar contagem de "N selecionados" — nunca deixar contagem inconsistente com o que está de fato marcado.
Ordenação em coluna com valores nulos/ausentes Definir e documentar uma posição consistente (nulos sempre no fim, por exemplo), não deixar ordenação "instável" (mudar de posição a cada re-render).
Seleção "todos" com paginação Deixar explícito se "selecionar tudo" abrange apenas a página atual ou o dataset inteiro, com uma ação secundária "selecionar todos os X itens" quando aplicável (padrão do Polaris IndexTable).
Redimensionamento de coluna abaixo do conteúdo mínimo Impedir redução além de uma largura mínima que preserve legibilidade do header; truncar conteúdo, não escondê-lo silenciosamente.
Virtualização + leitor de tela Sem aria-rowcount/aria-rowindex, o AT anuncia um total de linhas incorreto (apenas as renderizadas); obrigatório configurar esses atributos quando o DOM não reflete o dataset completo.
Ações em massa aplicadas a uma seleção grande (milhares de itens) Comunicar progresso/tempo esperado; nunca travar a UI sem feedback durante o processamento.

14. Boas práticas

Faça:

  • Sempre fornecer um aria-label ou <caption> descritivo para a tabela, mesmo quando o título visual já existe fora do <table> — a associação semântica precisa ser explícita.
  • Usar <th scope="col">/scope="row" (ou equivalentes ARIA em grid) para toda célula de cabeçalho, sem exceção.
  • Definir explicitamente se a tabela é table (estática) ou grid (interativa) antes de implementar — não misturar comportamentos dos dois padrões.
  • Persistir estado de ordenação/filtros na URL ou storage quando a navegação de volta à página deveria preservar contexto.
  • Testar a tabela com zoom de página em 400% e com um leitor de tela real (não apenas o axe DevTools) antes de considerar "pronta".

Evite:

  • Depender apenas de cor para comunicar status em célula (ex.: vermelho = erro) sem texto ou ícone acompanhando — falha WCAG 1.4.1 (uso de cor).
  • Colocar mais de uma ação primária por linha sem agrupar as secundárias em um menu overflow — cada botão extra compete por atenção e por área de toque.
  • Truncar nomes/identificadores de forma que múltiplos itens fiquem visualmente idênticos.
  • Reimplementar navegação por setas (grid) quando a tabela é puramente de leitura — overhead de acessibilidade e manutenção sem ganho real.

Não faça:

  • Não use <div>s estilizadas como tabela sem a semântica ARIA completa de grid/table — perde toda a navegação automática de tabela que leitores de tela já sabem fazer com HTML nativo.
  • Não esconda a única forma de acessar uma ação crítica atrás de um hover-only affordance — inacessível em touch e para navegação por teclado sem foco visível.
  • Não misture, na mesma tabela, paginação numérica e scroll infinito simultaneamente — os dois modelos mentais de "onde estou no dataset" competem entre si.

15. Análise comparativa de mercado

Material Design (M3)

O Material 3 mantém a Data table como componente documentado (m3.material.io/components), com anatomia explícita de seis partes: header row, rows, pagination, row checkbox, sort button e container. Especificações de densidade são dadas em valores absolutos (header row: 56dp; linha: 52dp; padding: 16dp), refletindo a abordagem historicamente mais prescritiva do Material em relação a medidas físicas. Notavelmente, a implementação de referência em código (@material/data-table, biblioteca web do Material Components) ainda carrega bastante herança do Material 2 — o ecossistema de Data Table nativo em Material 3 (Compose/Web) é menos maduro que o de outros componentes M3, e times frequentemente recorrem a bibliotecas de terceiros (ex.: material-table, MUI X Data Grid) para tabelas complexas em produção. Sorting dispara um evento (SORTED) e cabe ao consumidor reordenar os dados e ajustar os atributos ARIA de ordenação — ou seja, o Material trata a ordenação como headless (lógica fora do componente).

Carbon Design System (IBM)

Carbon documenta o DataTable com três variantes de interação (não detalhadas em profundidade aqui, mas cobrindo do mais simples ao mais avançado) e trata explicitamente a paginação como componente separado, composto com a tabela. A abordagem de acessibilidade do Carbon é declarada como "incorporada por padrão" — cabeçalhos de coluna ordenáveis são alcançáveis por teclado nativamente, e controles interativos dentro de células mantêm a ordem de tabulação normal, sugerindo que o Carbon trata a maior parte de seus data tables como HTML table semântico aprimorado, não como grid ARIA completo, mesmo em variantes com ordenação e expansão. Um dado relevante para este documento: o próprio time Carbon reconhece as limitações do DataTable interno (pouco composable) e vem explorando integração com TanStack Table (headless) como caminho para tabelas mais complexas — o mesmo movimento de "core simples + biblioteca headless para casos avançados" observado em Fluent UI.

Atlassian Design System

Achado relevante: o componente Table "puro" do Atlassian Design System está marcado como experimento deprioritizado, não recomendado para produção, com recomendação explícita de usar o Dynamic table no lugar — que já embute paginação, ordenação e reordenação de linhas prontas para uso, reduzindo a necessidade de compor manualmente. Além disso, o Atlassian mantém um componente dedicado Table tree para hierarquias expansíveis de dados (o caso de uso mais próximo de treegrid), separando claramente a preocupação "tabela plana" de "tabela hierárquica" em dois componentes distintos em vez de uma única API com muitas variantes.

Shopify Polaris

Polaris é o sistema com a distinção mais explícita entre dois casos de uso de tabela, cada um com componente próprio:

  • DataTable: para "apresentar pequenas quantidades de dados para visualização estática" — comparação e análise, com regras de alinhamento embutidas nos tipos de coluna (numérico → direita, textual → esquerda) e recomendação de linha de resumo/totais.
  • IndexTable: um "widget de tabela acionável, filtrável e ordenável que suporta seleção de linha com subheaders" — voltado a listas de recursos navegáveis (pedidos, produtos), com suporte avançado a seleção por intervalo (Shift+clique/Space), subheaders que agrupam blocos de linhas selecionáveis em conjunto, e paginação integrada.

Essa separação reforça a distinção que este documento propõe na seção 1 entre "tabela de leitura/comparação" e "tabela acionável" — Polaris não tenta resolver os dois casos com um único componente superconfigurável, e sim com dois componentes com API e propósito distintos. Ambos estão migrando para uma versão web component agnóstica de framework (<s-table>), que já embute o comportamento responsivo de "lista em telas pequenas, tabela em telas grandes" descrito na seção 10.

Adobe Spectrum (React Spectrum / Spectrum Web Components)

O Spectrum oferece tanto uma versão declarativa simples (<sp-table>, Spectrum Web Components) quanto a implementação mais sofisticada do comparativo: TableView (React Spectrum), construída sobre os hooks acessíveis do React Aria. É o único sistema do comparativo com documentação explícita e detalhada de comportamento de acessibilidade em nível de decisão de API:

  • Exige aria-label (ou aria-labelledby) obrigatoriamente na TableView.
  • Por padrão, a primeira coluna é usada como row header (anunciada ao navegar pelas linhas), com a prop isRowHeader disponível para customizar quais colunas devem rotular a linha — mapeando exatamente a recomendação de scope="row" do WAI-ARIA.
  • Redimensionamento de coluna (allowsResizing) foi entregue com suporte completo a mouse, teclado, touch e leitor de tela simultaneamente — evidência de que o time tratou a acessibilidade do redimensionamento como parte do escopo original da feature, não um adendo posterior.
  • Documentação pública de "falsos positivos de acessibilidade" conhecidos (ex.: uso de tabindex="-1" no corpo rolável, necessário para Firefox, mas sinalizado incorretamente por ferramentas automatizadas como o axe) — um lembrete valioso de que auditoria automatizada não substitui teste manual com leitor de tela real.

Fluent UI (Microsoft, v9)

Fluent UI separa deliberadamente dois níveis de abstração:

  • Table (@fluentui/react-table): primitivos de baixo nível, para quando o time precisa de customização além do padrão — a documentação recomenda explicitamente usar DataGrid "se você não precisa de muita customização".
  • DataGrid: componente opinativo e "inteligente", construído sobre os mesmos primitivos de Table combinados com o hook useTableFeatures (que compõe plugins como useTableSort e useTableSelection) — arquiteturalmente muito próximo do modelo headless popularizado pelo TanStack Table, mas mantido internamente pela própria Microsoft com acessibilidade de primeira classe já resolvida.

Times da comunidade relatam que, apesar da base sólida, DataGrid ainda carece de features "prontas" que outros frameworks entregam nativamente (paginação, filtragem avançada, virtualização confortável), levando a integrações externas com TanStack Table — o mesmo padrão de lacuna observado em Carbon e (para casos avançados) em Material.

Tabela-síntese

Sistema Nomenclatura Estática vs. Grid Paginação Observação distintiva
Material 3 Data table Majoritariamente estática, ordenação headless Integrada à anatomia do componente Ecossistema Data Table nativo M3 ainda imaturo; mercado recorre a libs externas
Carbon DataTable HTML table aprimorado, não grid pleno Componente separado, composto Time reconhece baixa composability e migra para TanStack Table
Atlassian Table (deprioritizado) → Dynamic table / Table tree Dynamic table (acionável) vs. Table tree (hierárquico) Integrada ao Dynamic table Único a desencorajar explicitamente seu componente de tabela "base"
Polaris (Shopify) DataTable (estático) vs. IndexTable (acionável) Separação explícita por caso de uso Integrada em ambos Migração ativa para web component <s-table> responsivo
Spectrum (Adobe) Table / TableView Grid pleno via React Aria (role="grid") Não nativa (composição externa) Documentação de acessibilidade mais granular e transparente do grupo
Fluent UI Table (primitivos) vs. DataGrid (opinativo) Table = table; DataGrid = grid, via composição de hooks Não nativa Arquitetura headless-first mais explícita do grupo

Diferenças e semelhanças: todos os seis sistemas convergem na distinção conceitual entre "tabela de leitura/comparação" e "tabela acionável/interativa" — mesmo quando não a expõem como dois componentes separados (caso do Material e do Carbon), a documentação de cada um reconhece a diferença de complexidade e de requisitos de acessibilidade entre os dois modos. A tendência mais recente e consistente entre os seis é a composição com bibliotecas de tabela headless (TanStack Table aparece explicitamente em Carbon, Material e Fluent) para lidar com filtragem, paginação server-side e virtualização — sinal de que a comunidade de design systems converge para "o design system fornece a casca visual e a acessibilidade; a lógica de estado de dados complexos vem de uma lib especializada". O aprendizado mais acionável para um design system novo é o padrão do Polaris e do Atlassian: nomear e separar deliberadamente "tabela estática" de "tabela acionável" (ou desencorajar a primeira em favor da segunda quando ela raramente é suficiente na prática), em vez de tentar uma única API "table" com dezenas de props condicionais.


16. Recomendações para Design Systems

UX: Adote a separação conceitual "tabela estática vs. tabela acionável" desde o início da API — mesmo que a implementação técnica compartilhe a maior parte do código internamente (como sugere a experiência de Polaris e Atlassian), a decisão de design de expor isso como uma escolha explícita (prop interactive/role) evita que times apliquem complexidade de grid "por segurança" em tabelas puramente de leitura.

Acessibilidade: Trate role="table" como o padrão seguro e só promova para role="grid" quando houver navegação célula-a-célula real. Torne obrigatórios na API, não opcionais: aria-label/caption, scope/row header em toda coluna identificadora, e aria-sort sempre que a coluna for ordenável. Para qualquer variante virtualizada, exija aria-rowcount/aria-rowindex como parte do contrato do componente, não como responsabilidade do consumidor lembrar de implementar.

Arquitetura: Separe paginação como componente composto (não acoplado ao corpo da tabela) — é a decisão mais consistente entre os seis sistemas pesquisados, e reduz o escopo de responsabilidade do componente Table em si. Para funcionalidades avançadas (edição inline, filtragem client-side complexa, virtualização de grandes datasets), considere adotar uma base headless (ex.: TanStack Table) por baixo da API visual do design system, seguindo o caminho já percorrido por Fluent UI, Carbon e o ecossistema Material — isso evita reimplementar lógica de estado que já é resolvida e testada por bibliotecas especializadas, concentrando o esforço do design system na camada visual, de tokens e de acessibilidade.

Implementação: Modele "seleção", "ordenação" e "expansão" como capacidades independentes habilitadas via props/composição, não como variantes visuais estanques — isso evita a proliferação combinatória de variantes (seção 9) e mantém a API previsível conforme o produto evolui. Prefira controlado por padrão para ordenação e paginação, já que a maioria dos casos reais de uso em produto envolve sincronizar esse estado com fetch de dados no backend.

Documentação: Documente explicitamente, com exemplos de código, os dois cenários (tabela estática vs. grid) lado a lado, incluindo o comportamento de teclado esperado para cada um — a ausência dessa distinção é a lacuna mais comum observada até em sistemas maduros. Inclua, na documentação de acessibilidade, casos de teste manual com leitor de tela (não apenas resultados de linter automatizado), inspirando-se na transparência do React Spectrum ao documentar até "falsos positivos" conhecidos de ferramentas de auditoria.


Referências

WAI-ARIA APG / WCAG:

Design systems: