Como construir tabelas de dados acessíveis e performáticas

1. Fundamentos de acessibilidade em tabelas de dados

A construção de tabelas acessíveis começa com a semântica HTML correta. Uma tabela bem estruturada permite que leitores de tela interpretem corretamente as relações entre dados e cabeçalhos.

<table>
  <caption>Vendas mensais por região - 2024</caption>
  <thead>
    <tr>
      <th scope="col">Mês</th>
      <th scope="col">Norte</th>
      <th scope="col">Sul</th>
      <th scope="col">Total</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Janeiro</th>
      <td>R$ 45.000</td>
      <td>R$ 52.000</td>
      <td>R$ 97.000</td>
    </tr>
    <tr>
      <th scope="row">Fevereiro</th>
      <td>R$ 48.000</td>
      <td>R$ 55.000</td>
      <td>R$ 103.000</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th scope="row">Total</th>
      <td>R$ 93.000</td>
      <td>R$ 107.000</td>
      <td>R$ 200.000</td>
    </tr>
  </tfoot>
</table>

Para tabelas com cabeçalhos complexos (mesclados), utilize id e headers:

<table>
  <caption>Notas dos alunos por disciplina</caption>
  <thead>
    <tr>
      <th id="aluno" rowspan="2">Aluno</th>
      <th id="matematica" colspan="2">Matemática</th>
      <th id="portugues" colspan="2">Português</th>
    </tr>
    <tr>
      <th id="mat-nota" headers="matematica">Nota</th>
      <th id="mat-falta" headers="matematica">Faltas</th>
      <th id="por-nota" headers="portugues">Nota</th>
      <th id="por-falta" headers="portugues">Faltas</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th headers="aluno" scope="row">Ana Silva</th>
      <td headers="matematica mat-nota">8.5</td>
      <td headers="matematica mat-falta">2</td>
      <td headers="portugues por-nota">9.0</td>
      <td headers="portugues por-falta">1</td>
    </tr>
  </tbody>
</table>

Atributos ARIA complementares:

<div role="table" aria-label="Estatísticas de produção" aria-describedby="table-desc">
  <p id="table-desc">Dados de produção mensal das fábricas, ordenados por eficiência.</p>
  <div role="rowgroup">
    <div role="row">
      <span role="columnheader" aria-sort="ascending">Fábrica</span>
      <span role="columnheader">Produção</span>
    </div>
  </div>
</div>

2. Navegação por teclado e foco visual

Implemente navegação por teclado com roving tabindex para células interativas:

<table id="tabela-interativa">
  <thead>
    <tr>
      <th>Nome</th>
      <th>Cargo</th>
      <th>Ações</th>
    </tr>
  </thead>
  <tbody>
    <tr tabindex="0" data-row="1">
      <td>João</td>
      <td>Analista</td>
      <td><button tabindex="-1" class="editar">Editar</button></td>
    </tr>
    <tr tabindex="-1" data-row="2">
      <td>Maria</td>
      <td>Gerente</td>
      <td><button tabindex="-1" class="editar">Editar</button></td>
    </tr>
  </tbody>
</table>

Estilização de foco visível com contraste adequado:

tr:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
  background-color: #e8f0fe;
}

td:focus-visible,
th:focus-visible {
  outline: 2px solid #005fcc;
  outline-offset: -2px;
}

Atalhos de teclado para navegação entre células:

document.addEventListener('keydown', function(e) {
  const tabela = document.getElementById('tabela-interativa');
  const celulaAtual = document.activeElement;

  if (!tabela.contains(celulaAtual)) return;

  if (e.key === 'ArrowDown') moverCelula(celulaAtual, 0, 1);
  if (e.key === 'ArrowUp') moverCelula(celulaAtual, 0, -1);
  if (e.key === 'ArrowLeft') moverCelula(celulaAtual, -1, 0);
  if (e.key === 'ArrowRight') moverCelula(celulaAtual, 1, 0);
  if (e.key === 'Home') irParaPrimeiraCelula();
  if (e.key === 'End') irParaUltimaCelula();
});

3. Performance na renderização de grandes volumes de dados

Para tabelas com milhares de linhas, implemente virtualização:

<div id="container-tabela" style="height: 500px; overflow-y: auto;">
  <table style="position: relative;">
    <thead style="position: sticky; top: 0; z-index: 1;">
      <tr>
        <th>ID</th>
        <th>Nome</th>
        <th>Valor</th>
      </tr>
    </thead>
    <tbody id="tbody-virtual">
      <!-- Apenas linhas visíveis são renderizadas -->
    </tbody>
  </table>
</div>

Utilize content-visibility para otimizar renderização:

tr {
  content-visibility: auto;
  contain: layout style paint;
}

tr:not(:has(td)) {
  content-visibility: visible;
}

Implementação com Intersection Observer para carregamento progressivo:

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const linha = entry.target;
      carregarDadosLinha(linha.dataset.index);
      observer.unobserve(linha);
    }
  });
}, { rootMargin: '200px' });

document.querySelectorAll('tr[data-lazy]').forEach(linha => {
  observer.observe(linha);
});

4. Ordenação, filtragem e busca responsivas

Implemente ordenação com suporte a leitores de tela:

<th scope="col" aria-sort="ascending" data-coluna="nome">
  Nome
  <button class="btn-ordem" aria-label="Ordenar por nome em ordem decrescente">▲</button>
</th>

Filtros com debounce e notificação ao vivo:

<input type="text" id="filtro" placeholder="Filtrar por nome..." aria-label="Filtrar tabela">
<div id="resultados" aria-live="polite" aria-atomic="true" class="sr-only">
  15 resultados encontrados
</div>

Realce de resultados com navegação entre ocorrências:

function buscarNaTabela(termo) {
  const celulas = document.querySelectorAll('td');
  let ocorrencias = [];

  celulas.forEach(celula => {
    const texto = celula.textContent.toLowerCase();
    if (texto.includes(termo.toLowerCase())) {
      celula.innerHTML = celula.textContent.replace(
        new RegExp(termo, 'gi'),
        match => `<mark>${match}</mark>`
      );
      ocorrencias.push(celula);
    }
  });

  return ocorrencias;
}

5. Design responsivo e adaptação para dispositivos móveis

Tabela com rolagem horizontal e indicação visual:

<div class="tabela-wrapper" style="overflow-x: auto; position: relative;">
  <div class="sombra-esquerda"></div>
  <table style="min-width: 800px;">
    <!-- conteúdo da tabela -->
  </table>
  <div class="sombra-direita"></div>
</div>

Transformação para card view em dispositivos móveis:

@media (max-width: 768px) {
  table, thead, tbody, th, td, tr {
    display: block;
  }

  thead tr {
    position: absolute;
    top: -9999px;
    left: -9999px;
  }

  tr {
    margin-bottom: 1rem;
    border: 1px solid #ccc;
    border-radius: 8px;
    padding: 0.5rem;
  }

  td {
    display: flex;
    justify-content: space-between;
    padding: 0.5rem;
    border-bottom: 1px solid #eee;
  }

  td::before {
    content: attr(data-label);
    font-weight: bold;
    margin-right: 1rem;
  }
}

6. Otimização de assets e carregamento preguiçoso

Carregamento sob demanda com scroll:

let paginaAtual = 1;
const tamanhoPagina = 50;

document.getElementById('container-tabela').addEventListener('scroll', function() {
  if (this.scrollTop + this.clientHeight >= this.scrollHeight - 100) {
    paginaAtual++;
    carregarPagina(paginaAtual);
  }
});

function carregarPagina(pagina) {
  fetch(`/api/dados?pagina=${pagina}&limite=${tamanhoPagina}`)
    .then(response => response.json())
    .then(dados => {
      const fragment = document.createDocumentFragment();
      dados.forEach(item => {
        const linha = criarLinha(item);
        fragment.appendChild(linha);
      });
      document.getElementById('tbody-virtual').appendChild(fragment);
    });
}

Cache de dados ordenados/filtrados no cliente:

const cache = new Map();

function obterDadosCache(chave, funcaoCarregamento) {
  if (cache.has(chave)) {
    return Promise.resolve(cache.get(chave));
  }

  return funcaoCarregamento().then(dados => {
    cache.set(chave, dados);
    return dados;
  });
}

7. Testes de acessibilidade e validação contínua

Checklist WCAG 2.1 AA para tabelas:

1. Estrutura semântica correta (SC 1.3.1)
2. Cabeçalhos associados a dados (SC 1.3.1)
3. Contraste mínimo de 4.5:1 (SC 1.4.3)
4. Foco visível (SC 2.4.7)
5. Navegação por teclado (SC 2.1.1)
6. Mensagens de status para leitores de tela (SC 4.1.3)
7. Redimensionamento até 200% sem perda (SC 1.4.4)
8. Ordem de tabulação lógica (SC 2.4.3)

Testes automatizados com axe-core:

const resultado = await axe.run(document.getElementById('tabela'), {
  rules: {
    'td-headers-attr': { enabled: true },
    'th-scope': { enabled: true },
    'table-fake-caption': { enabled: true }
  }
});

resultado.violations.forEach(violation => {
  console.error(`Violação: ${violation.description}`);
  violation.nodes.forEach(node => {
    console.error(`  Elemento: ${node.html}`);
  });
});

Referências