Table

Pronto

Listagem tabular com header sticky, hover de linha, sort por coluna, seleção múltipla e ações por linha. Otimizada para densidade jurídica (CNJ, datas, valores monetários).

Usar quando

Listas de processos, contatos, lançamentos financeiros, intimações. Quando comparação por coluna importa.

Não usar quando

Cards visuais (use grid de Cards). Lista de poucos itens (use list simples). Mobile (use lista vertical com touch-friendly rows).

Variantes

Padrão (sortable + hover + ações)
Número CNJPartesÁreaStatusValorPrazo
0001234-56.2026.8.26.0100João Silva vs Acme LtdaTrabalhistaEm andamentoR$ 45.000,0015/05/2026
0007654-32.2025.5.02.0001Maria SantosCívelAguardandoR$ 120.000,0022/05/2026
0009876-54.2026.3.01.0050Carlos Pereira vs Tech Co.TributárioEm andamentoR$ 89.500,0003/06/2026
0001111-22.2024.8.26.0200Ana CostaFamíliaEncerradoR$ 25.000,00
0005555-66.2026.4.03.6100Globo ComércioEmpresarialEm andamentoR$ 350.000,0010/05/2026
Com seleção múltipla (checkbox + bulk actions)
Número CNJPartesÁreaStatusValorPrazo
0001234-56.2026.8.26.0100João Silva vs Acme LtdaTrabalhistaEm andamentoR$ 45.000,0015/05/2026
0007654-32.2025.5.02.0001Maria SantosCívelAguardandoR$ 120.000,0022/05/2026
0009876-54.2026.3.01.0050Carlos Pereira vs Tech Co.TributárioEm andamentoR$ 89.500,0003/06/2026
0001111-22.2024.8.26.0200Ana CostaFamíliaEncerradoR$ 25.000,00
0005555-66.2026.4.03.6100Globo ComércioEmpresarialEm andamentoR$ 350.000,0010/05/2026
Densa (rows compactas — 32px)
Número CNJPartesÁreaStatusValorPrazo
0001234-56.2026.8.26.0100João Silva vs Acme LtdaTrabalhistaEm andamentoR$ 45.000,0015/05/2026
0007654-32.2025.5.02.0001Maria SantosCívelAguardandoR$ 120.000,0022/05/2026
0009876-54.2026.3.01.0050Carlos Pereira vs Tech Co.TributárioEm andamentoR$ 89.500,0003/06/2026
0001111-22.2024.8.26.0200Ana CostaFamíliaEncerradoR$ 25.000,00
0005555-66.2026.4.03.6100Globo ComércioEmpresarialEm andamentoR$ 350.000,0010/05/2026

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Row hovermouseenter rowBackground → surfaceHover, ações revelam100ms ease
Sort clickclick no headerSetinha gira ↑↓ + reordena com fade-cross200ms cubic-bezier(0.32,0.72,0,1)
Select allcheck no headerCascata visual: linhas marcam top-down50ms stagger
Bulk action barseleção > 0Bar escorrega de cima com slide-down250ms ease
Resize column (advanced)drag entre colunasColuna ajusta width, cursor col-resizefollows pointer
Empty state0 resultadosSubstitui tbody por <EmptyState> inlineinstant
Focused row keyboard↑↓ com focoBackground ring brass-edge, scroll automáticoinstant

Acessibilidade

Acessibilidade — checklist

Teclado
TabFoco na tabela
↑ / ↓Navega linhas
← / →Navega células
Space (header)Toggle sort
Space (row)Toggle seleção
Cmd/Ctrl+ASelecionar todos
EnterAbre detalhe (drill-down)
ARIA esperado
  • <table role="table"><thead><tr role="row"><th role="columnheader" aria-sort="ascending" />
  • <tbody><tr role="row" aria-selected="true|false"><td role="cell" /></tr>
  • caption acima da tabela com aria-label
  • Seleção: aria-multiselectable="true" no role="grid"
Notas
  • Header sticky em scroll vertical (top: 0 + position: sticky).
  • Sort: ícone setinha próxima ao label, gira na mudança.
  • Multi-select com Shift+Click suporta range.
  • Em mobile: deserialize tabela em lista vertical.
  • Datas e valores monetários SEMPRE em mono (tabular-nums alinha decimal).

Código

'use client';
import { useState } from 'react';
import { ArrowUp, ArrowDown } from 'lucide-react';
import { T, TYPE, SP } from '@/lib/tokens';

interface Column<T> {
  key: keyof T;
  label: string;
  sortable?: boolean;
  render?: (row: T) => React.ReactNode;
  align?: 'left' | 'right';
}

export function Table<T>({ columns, rows }: { columns: Column<T>[]; rows: T[] }) {
  const [sort, setSort] = useState<{ key: keyof T; dir: 'asc' | 'desc' } | null>(null);
  const sorted = sort
    ? [...rows].sort((a, b) => {
        const va = a[sort.key], vb = b[sort.key];
        return sort.dir === 'asc' ? (va > vb ? 1 : -1) : (va < vb ? 1 : -1);
      })
    : rows;

  return (
    <table style={{ width: '100%', borderCollapse: 'collapse' }}>
      <thead>
        <tr style={{ background: T.surface2 }}>
          {columns.map((col) => (
            <th key={String(col.key)}
              onClick={() => col.sortable && setSort({ key: col.key, dir: sort?.dir === 'asc' ? 'desc' : 'asc' })}>
              {col.label}
              {sort?.key === col.key && (sort.dir === 'asc' ? <ArrowUp size={10} /> : <ArrowDown size={10} />)}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>{sorted.map((r, i) => <tr key={i}>...</tr>)}</tbody>
    </table>
  );
}

Regras

Faça

  • Header sticky no scroll vertical.
  • Hover de linha em surfaceHover (suave).
  • Datas e valores em font-mono + tabular-nums.
  • Ações por linha aparecem em hover (não polir layout em rest).
  • Sort com ícone setinha visível, default ASC primeiro click, DESC segundo, OFF terceiro.
  • Seleção múltipla: header com indeterminate quando parcial.

Não faça

  • Não use bordas em todas as células (use só borderTop entre linhas).
  • Não force colunas estreitas (mínimo confortável para conteúdo).
  • Não esconda o sort (seta deve indicar coluna ativa).
  • Não use tabela para layout (use grid CSS).
  • Não esqueça empty state.
  • Não tenha mais de 7-8 colunas visíveis (ofereça toggle/scroll horizontal).

Tokens usados

TokenValorPapel
T.surface#FFFFFFbackground
T.surface2#FAF8F2background do header
T.surfaceHover#F7F5EChover de linha
T.borderFaint#F0EEE5borda entre linhas
TYPE.base13fontSize célula
FONT.monoJetBrains Monodatas e números