# Table

Tabela semântica, com `<table>` de verdade.

Compõe com `TableCaption`, `TableHeader`, `TableBody`, `TableFooter`,
`TableRow`, `TableHead` e `TableCell`.

`selected` na linha desenha uma barra de acento na lateral e abre a primeira
célula com um marcador de texto que só o leitor de tela ouve: "Selecionada",
trocável por `labels.selected`. Cor sozinha não é estado.

Ela não marca no `aria-selected`. Esse atributo só vale dentro de `grid` ou
`treegrid`; num `<table>` simples o navegador o descarta, e o estado nunca
chega ao leitor. Virar `grid` custaria caro: `grid` exige navegação por setas
entre as células, que esta peça não implementa. O marcador textual entrega o
estado sem prometer um teclado que não existe.

A moldura rola de lado sozinha, então tabela larga não empurra a página.

O nome da tabela entra pelo `TableCaption`, e não por um `<h3>` acima dela: o
título vizinho não nomeia elemento nenhum, e o leitor de tela anuncia só
"tabela, 5 colunas".

A linha de totais entra pelo `TableFooter`, e não por uma `<div>` embaixo da
tabela: dentro do `<tfoot>` a célula divide a largura com a coluna, e o total
fica debaixo do valor que ele soma.

## Quando não usar

Para listagem que vem de uma consulta, use `DataTable`. Ela trata os três
estados que toda consulta tem e quase nenhuma tabela escrita à mão trata
(carregando, erro e vazio) e traz ordenação, busca, paginação e seleção sem
nada disso virar estado da sua tela.

Este aqui fica para a tabela que você desenha: o quadro de valores de um
recibo, a comparação de planos, a linha de totais montada à mão. Quando as
linhas são um `map` sobre o que a API devolveu, é a outra.

## No React Native

Não porta, por decisão - não há tabela no celular; a consulta vira `DataList`. Não é fila: não vai existir. A [tabela de paridade](/react-native) diz o porquê de cada uma.

## Importação

```tsx
import { Table } from '@rivocode/ui'
```

## Exemplos

### Listagem

```tsx
import {
  Badge,
  Checkbox,
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from '@rivocode/ui'
import { currencyShort } from '@rivocode/ui/chart'

export function Listing() {
  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead className="w-10">
            <Checkbox indeterminate aria-label="Selecionar todas" />
          </TableHead>
          <TableHead>Número</TableHead>
          <TableHead>Cliente</TableHead>
          <TableHead>Status</TableHead>
          <TableHead className="text-right">Valor</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        <TableRow>
          <TableCell><Checkbox aria-label="Selecionar 4812" /></TableCell>
          <TableCell className="font-mono text-sm text-fg-muted">4812</TableCell>
          <TableCell>Prefeitura de João Pessoa</TableCell>
          <TableCell><Badge tone="success" size="sm">Paga</Badge></TableCell>
          <TableCell className="text-right font-mono">{currencyShort(12400)}</TableCell>
        </TableRow>
        <TableRow selected>
          <TableCell><Checkbox checked aria-label="Selecionar 4813" /></TableCell>
          <TableCell className="font-mono text-sm text-fg-muted">4813</TableCell>
          <TableCell>Clínica São Lucas</TableCell>
          <TableCell><Badge tone="info" size="sm">Aberta</Badge></TableCell>
          <TableCell className="text-right font-mono">{currencyShort(3300)}</TableCell>
        </TableRow>
        <TableRow>
          <TableCell><Checkbox aria-label="Selecionar 4814" /></TableCell>
          <TableCell className="font-mono text-sm text-fg-muted">4814</TableCell>
          <TableCell>Transportes Cabo Branco</TableCell>
          <TableCell><Badge tone="danger" size="sm">Vencida</Badge></TableCell>
          <TableCell className="text-right font-mono">{currencyShort(8800)}</TableCell>
        </TableRow>
      </TableBody>
    </Table>
  )
}
```

### Com linha de totais

```tsx
import {
  Badge,
  Checkbox,
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from '@rivocode/ui'
import { currencyShort } from '@rivocode/ui/chart'

export function WithTotals() {
  const invoices = [
    { number: '4812', customer: 'Prefeitura de João Pessoa', amount: 12_400 },
    { number: '4813', customer: 'Clínica São Lucas', amount: 3300 },
    { number: '4814', customer: 'Transportes Cabo Branco', amount: 8800 },
  ]

  const total = invoices.reduce((sum, invoice) => sum + invoice.amount, 0)

  return (
    <Table>
      <TableHeader>
        <TableRow>
          <TableHead>Número</TableHead>
          <TableHead>Cliente</TableHead>
          <TableHead className="text-right">Valor</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.number}>
            <TableCell className="font-mono text-sm text-fg-muted">{invoice.number}</TableCell>
            <TableCell>{invoice.customer}</TableCell>
            <TableCell className="text-right font-mono">{currencyShort(invoice.amount)}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      {/* Num <tfoot>, e nao numa <div> embaixo: a célula divide a largura com
          a coluna, então o total fica debaixo do valor que ele soma. */}
      <TableFooter>
        <TableRow>
          <TableCell colSpan={2}>Total</TableCell>
          <TableCell className="text-right font-mono">{currencyShort(total)}</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  )
}
```

### Com legenda

```tsx
import {
  Badge,
  Checkbox,
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from '@rivocode/ui'
import { currencyShort } from '@rivocode/ui/chart'

export function WithCaption() {
  const payments = [
    { id: 'PIX-9021', method: 'Pix', amount: 4200 },
    { id: 'BOL-4477', method: 'Boleto', amount: 1850 },
    { id: 'CTC-1180', method: 'Cartão de crédito', amount: 990 },
  ]

  return (
    <Table>
      {/* Primeiro filho da <table>, e não uma <p> acima dela: o título vizinho
          não nomeia elemento nenhum, e o leitor de tela anuncia só "tabela, 3
          colunas". */}
      <TableCaption>Pagamentos recebidos em junho de 2025</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Identificador</TableHead>
          <TableHead>Meio</TableHead>
          <TableHead className="text-right">Valor</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {payments.map((payment) => (
          <TableRow key={payment.id}>
            <TableCell className="font-mono text-sm text-fg-muted">{payment.id}</TableCell>
            <TableCell>{payment.method}</TableCell>
            <TableCell className="text-right font-mono">{currencyShort(payment.amount)}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

## Props

Não tem prop própria. Repassa `className`, `style`, `id` e os demais atributos do elemento raiz.

## Partes

O componente se monta com as peças abaixo. Todas vêm de `@rivocode/ui`.

### TableBody

O `<tbody>`: as linhas de dado.

Não desenha nada por conta própria. O visual da linha mora no `TableRow`.

### TableCaption

A legenda da tabela, num `<caption>` de verdade. É o nome que o leitor de tela
anuncia antes de entrar nas linhas.

Ela vai **dentro** do `Table`, e como primeiro filho:

```tsx
<Table>
  <TableCaption>Notas emitidas em junho de 2025</TableCaption>
  <TableHeader>…</TableHeader>
  <TableBody>…</TableBody>
  <TableFooter>…</TableFooter>
</Table>
```

O instinto é escrever esse título numa `<p>` ou num `<h3>` logo acima da
tabela. Não quebra nada, e custa o nome inteiro: o anúncio vira "tabela, 5
colunas, 12 linhas" e mais nada, porque texto vizinho não nomeia elemento
nenhum. Numa tela com duas tabelas (a das notas e a dos pagamentos), quem
navega por lista de tabelas ouve as duas com o mesmo nome, que é nome nenhum.

E não há meio-termo: `<caption>` não tem outro pai legal além de `<table>`.
Solto ao lado do `Table`, onde o título parece caber melhor, o React derruba a
tela:

```
In HTML, <caption> cannot be a child of <div>. This will cause a hydration error.
```

Ou é filho da `<table>`, ou não é legenda.

Quando a tabela já tem um título na página, a legenda continua valendo a pena
como nome, só que sem ocupar pixel nenhum:

```tsx
<TableCaption className="sr-only">Notas emitidas em junho de 2025</TableCaption>
```

É o que o `DataTable` faz com o `caption` dele.

A legenda sai em cima. Para mandá-la para baixo da tabela, `caption-bottom` na
classe. O `caption-top` da peça sai do caminho sozinho.

## Quando não usar

Numa listagem que vem de uma consulta, não monte o `<caption>` à mão: o
`DataTable` recebe a legenda pela prop `caption`, e já a escreve dentro da
`<table>` certa (inclusive na variante com altura, onde a tabela é outra).

Para o título visível que encabeça a seção inteira, com ação do lado, é o
`PageHeader`, e não a legenda: `TableCaption` nomeia a tabela, não a tela.

### TableCell

Uma célula de dado.

Respiro e altura vêm da densidade, então a mesma tabela encolhe junto com o
resto em `density="compact"`. Para número, alinhe à direita e use fonte mono
pela `className`.

### TableFooter

O rodapé da tabela, num `<tfoot>` de verdade. É onde mora a linha de totais.

Toda listagem financeira daqui termina em "Total: R$ 248,3K", e até agora essa
linha era uma `<div>` embaixo da tabela. Uma `<div>` não participa do algoritmo
de layout de tabela: ela não conhece a largura de nenhuma coluna, então o total
nunca fica debaixo do valor que ele soma. E, numa tabela com moldura própria,
ela rola embora junto com o conteúdo.

Dentro da tabela as duas coisas se resolvem sozinhas: a célula divide a largura
com a coluna, e o rodapé pode grudar embaixo pelo mesmo mecanismo com que o
`TableHeader` gruda em cima.

```tsx
<Table>
  <TableHeader>…</TableHeader>
  <TableBody>…</TableBody>
  <TableFooter>
    <TableRow>
      <TableCell colSpan={2}>Total</TableCell>
      <TableCell className="text-right font-mono">{currencyShort(total)}</TableCell>
    </TableRow>
  </TableFooter>
</Table>
```

O peso é proposital: o rodapé é resumo, e resumo não pode se ler como mais uma
linha de dado.

**O dinheiro sai abreviado**, como no resto da casa: `currencyShort`, e não o
valor por extenso. O `currency` fica para onde o centavo é o assunto: o valor
que a pessoa confirma antes de emitir, e o comprovante depois.

Para grudar o rodapé numa tabela que rola por dentro, o `sticky` vai na classe,
com o degrau de empilhamento da família:

```tsx
<TableFooter className="sticky bottom-0 z-[var(--rc-z-sticky)] bg-surface">
```

O fundo não é opcional: sem ele a linha que passa por baixo aparece através do
rodapé.

## Quando não usar

Numa listagem que vem de uma consulta, não monte o `<tfoot>` à mão: o
`DataTable` produz a linha sozinho, a partir do `total` de cada coluna, e ali
ele já sabe quais linhas somar, qual coluna esconder no celular e quando grudar.

### TableHead

Uma célula de cabeçalho.

Sai como `<th>`, em caixa alta e menor, com a altura vinda da densidade. Não
quebra linha: cabeçalho quebrado desalinha a coluna toda.

### TableHeader

O `<thead>` da tabela, com a linha que separa do corpo.

Sempre com `TableRow` e `TableHead` dentro, cabeçalho montado com `<div>` some
para quem navega a tabela pelo leitor de tela.

### TableRow

Uma linha da tabela.

`selected` desenha uma barra de acento na lateral, com fundo tênue. A barra é
que diz "esta linha", e fundo forte mancha a leitura da linha inteira.

Cor sozinha não é estado, então a primeira célula da linha escolhida abre com
um `<span>` que só o leitor de tela ouve: "Selecionada". É sempre a primeira
célula, para quem ouve saber onde o aviso aparece. `labels.selected` troca o
texto quando a tela está em outro idioma.

Não há `aria-selected` aqui. Ele só é válido dentro de `grid` ou `treegrid`, e
num `<table>` simples o navegador descarta o atributo: medido na árvore de
acessibilidade, a linha expunha zero propriedades. Adotar `role="grid"` traria
a obrigação de navegar por setas entre as células, que a peça não implementa,
e trocaria um defeito por outro maior.

| Prop | Tipo | Obrigatória | Desde | O que faz |
| --- | --- | --- | --- | --- |
| `labels` | `{ selected?: string \| undefined; }` |  | - | O texto do marcador da linha escolhida. |
| `selected` | `boolean` |  | 0.4.0 | Linha escolhida. |

## Ver também

- [Accordion](/componentes/accordion.md)
- [AspectRatio](/componentes/aspect-ratio.md)
- [Avatar](/componentes/avatar.md)
- [Card](/componentes/card.md)
- [Collapsible](/componentes/collapsible.md)
- [DataTable](/componentes/data-table.md)
- [Convenções da biblioteca](/convencoes.md): Provider, tokens e as regras que valem para toda peça
- [Índice completo](/llms.txt)
