Table
Exibe dados em linhas e colunas, com ordenação, seleção por checkbox e navegação por teclado.
| Pedido | Cliente | Serviço | Valor | Entrega |
|---|---|---|---|---|
| #1042 | Padaria Pão de Ouro | Site institucional | R$ 3.200,00 | 10/10/2026 |
| #1043 | Clínica Sorriso | Landing page + Google Ads | R$ 4.800,00 | 18/10/2026 |
| #1044 | Lima Advocacia | Sistema de agendamento | R$ 12.900,00 | 05/11/2026 |
| #1045 | Pet Shop Amigo | Loja virtual | R$ 8.600,00 | 22/11/2026 |
'use client';
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@/components/ui/table';
const pedidos = [
{ id: '1042', cliente: 'Padaria Pão de Ouro', servico: 'Site institucional', valor: 3200, entrega: '10/10/2026' },
{ id: '1043', cliente: 'Clínica Sorriso', servico: 'Landing page + Google Ads', valor: 4800, entrega: '18/10/2026' },
{ id: '1044', cliente: 'Lima Advocacia', servico: 'Sistema de agendamento', valor: 12900, entrega: '05/11/2026' },
{ id: '1045', cliente: 'Pet Shop Amigo', servico: 'Loja virtual', valor: 8600, entrega: '22/11/2026' },
];
const brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
export default function TableDemo() {
return (
<Table aria-label="Pedidos em andamento">
<TableHeader>
<Column isRowHeader>Pedido</Column>
<Column>Cliente</Column>
<Column>Serviço</Column>
<Column className="text-right">Valor</Column>
<Column>Entrega</Column>
</TableHeader>
<TableBody items={pedidos}>
{(p) => (
<Row id={p.id}>
<Cell className="font-mono tabular-nums">#{p.id}</Cell>
<Cell className="font-medium">{p.cliente}</Cell>
<Cell className="text-muted-foreground">{p.servico}</Cell>
<Cell className="text-right font-mono tabular-nums">{brl.format(p.valor)}</Cell>
<Cell className="tabular-nums">{p.entrega}</Cell>
</Row>
)}
</TableBody>
</Table>
);
}Instalação
npx shadcn@latest add @lenstech/tableUso
import { Cell, Column, Row, Table, TableBody, TableHeader } from "@/components/ui/table"<Table aria-label="Pedidos">
<TableHeader>
<Column isRowHeader>Cliente</Column>
<Column>Valor</Column>
</TableHeader>
<TableBody items={pedidos}>
{(pedido) => (
<Row id={pedido.id}>
<Cell>{pedido.cliente}</Cell>
<Cell>{pedido.valor}</Cell>
</Row>
)}
</TableBody>
</Table>Toda tabela precisa de um nome e de uma coluna principal
Informe aria-label (ou aria-labelledby) no Table e marque a coluna que identifica a linha com
isRowHeader — é ela que o leitor de tela anuncia ao navegar pelas linhas.
Exemplos
Ordenação
Marque as colunas com allowsSorting e controle sortDescriptor / onSortChange. A tabela só informa
a coluna e a direção pedidas — a ordenação dos dados é feita por você (no cliente ou na API).
| Cliente | Serviço | Valor |
|---|---|---|
| Lima Advocacia | Sistema de agendamento | R$ 12.900,00 |
| Pet Shop Amigo | Loja virtual | R$ 8.600,00 |
| Clínica Sorriso | Landing page + Google Ads | R$ 4.800,00 |
| Padaria Pão de Ouro | Site institucional | R$ 3.200,00 |
| Studio JM | Identidade visual | R$ 2.400,00 |
'use client';
import * as React from 'react';
import type { SortDescriptor } from 'react-aria-components';
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@/components/ui/table';
const pedidos = [
{ id: '1042', cliente: 'Padaria Pão de Ouro', servico: 'Site institucional', valor: 3200 },
{ id: '1043', cliente: 'Clínica Sorriso', servico: 'Landing page + Google Ads', valor: 4800 },
{ id: '1044', cliente: 'Lima Advocacia', servico: 'Sistema de agendamento', valor: 12900 },
{ id: '1045', cliente: 'Pet Shop Amigo', servico: 'Loja virtual', valor: 8600 },
{ id: '1046', cliente: 'Studio JM', servico: 'Identidade visual', valor: 2400 },
];
type Pedido = (typeof pedidos)[number];
const brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
export default function TableSorting() {
const [sort, setSort] = React.useState<SortDescriptor>({ column: 'valor', direction: 'descending' });
// A tabela só informa qual coluna/direção foi pedida — a ordenação dos dados é sua.
const rows = React.useMemo(() => {
const key = sort.column as keyof Pedido;
return [...pedidos].sort((a, b) => {
const av = a[key];
const bv = b[key];
const cmp =
typeof av === 'number' && typeof bv === 'number' ? av - bv : String(av).localeCompare(String(bv), 'pt-BR');
return sort.direction === 'descending' ? -cmp : cmp;
});
}, [sort]);
return (
<Table aria-label="Pedidos" sortDescriptor={sort} onSortChange={setSort}>
<TableHeader>
<Column id="cliente" isRowHeader allowsSorting>
Cliente
</Column>
<Column id="servico">Serviço</Column>
<Column id="valor" allowsSorting className="text-right">
Valor
</Column>
</TableHeader>
<TableBody items={rows}>
{(p) => (
<Row id={p.id}>
<Cell className="font-medium">{p.cliente}</Cell>
<Cell className="text-muted-foreground">{p.servico}</Cell>
<Cell className="text-right font-mono tabular-nums">{brl.format(p.valor)}</Cell>
</Row>
)}
</TableBody>
</Table>
);
}Seleção
Com selectionMode="multiple" a coluna de checkboxes aparece sozinha, inclusive o "selecionar todos" no cabeçalho.
O valor de selectedKeys é um Set com os id das linhas — ou a string "all".
1 fatura(s) selecionada(s)
| Fatura | Cliente | Vencimento | Valor | |
|---|---|---|---|---|
| NF-2031 | Padaria Pão de Ouro | 05/10/2026 | R$ 890,00 | |
| NF-2032 | Clínica Sorriso | 08/10/2026 | R$ 1.490,00 | |
| NF-2033 | Lima Advocacia | 12/10/2026 | R$ 2.350,00 | |
| NF-2034 | Pet Shop Amigo | 15/10/2026 | R$ 640,00 |
'use client';
import * as React from 'react';
import type { Selection } from 'react-aria-components';
import { Button } from '@/components/ui/button';
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@/components/ui/table';
const faturas = [
{ id: 'NF-2031', cliente: 'Padaria Pão de Ouro', vencimento: '05/10/2026', valor: 890 },
{ id: 'NF-2032', cliente: 'Clínica Sorriso', vencimento: '08/10/2026', valor: 1490 },
{ id: 'NF-2033', cliente: 'Lima Advocacia', vencimento: '12/10/2026', valor: 2350 },
{ id: 'NF-2034', cliente: 'Pet Shop Amigo', vencimento: '15/10/2026', valor: 640 },
];
const brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
export default function TableSelection() {
const [selected, setSelected] = React.useState<Selection>(new Set(['NF-2032']));
const count = selected === 'all' ? faturas.length : selected.size;
return (
<div className="flex w-full flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-muted-foreground" aria-live="polite">
{count === 0 ? 'Nenhuma fatura selecionada' : `${count} fatura(s) selecionada(s)`}
</p>
<Button size="sm" variant="outline" isDisabled={count === 0}>
Enviar lembrete
</Button>
</div>
<div className="rounded-md border">
<Table
aria-label="Faturas em aberto"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
>
<TableHeader>
<Column isRowHeader>Fatura</Column>
<Column>Cliente</Column>
<Column>Vencimento</Column>
<Column className="text-right">Valor</Column>
</TableHeader>
<TableBody items={faturas}>
{(f) => (
<Row id={f.id}>
<Cell className="font-mono tabular-nums">{f.id}</Cell>
<Cell className="font-medium">{f.cliente}</Cell>
<Cell className="tabular-nums">{f.vencimento}</Cell>
<Cell className="text-right font-mono tabular-nums">{brl.format(f.valor)}</Cell>
</Row>
)}
</TableBody>
</Table>
</div>
</div>
);
}Estado vazio
Use renderEmptyState no TableBody para explicar por que não há dados e, se possível, o que fazer a seguir.
| Cliente | Serviço | Valor |
|---|---|---|
Nenhum orçamento enviado ainda Os orçamentos aparecem aqui assim que forem enviados ao cliente. | ||
'use client';
import { Inbox } from 'lucide-react';
import { Cell, Column, Row, Table, TableBody, TableHeader } from '@/components/ui/table';
type Orcamento = { id: string; cliente: string; servico: string; valor: string };
const orcamentos: Orcamento[] = [];
export default function TableEmpty() {
return (
<Table aria-label="Orçamentos enviados">
<TableHeader>
<Column isRowHeader>Cliente</Column>
<Column>Serviço</Column>
<Column className="text-right">Valor</Column>
</TableHeader>
<TableBody
items={orcamentos}
renderEmptyState={() => (
<div className="flex flex-col items-center gap-2 py-6">
<Inbox aria-hidden className="size-6 text-muted-foreground" />
<p className="font-medium text-foreground">Nenhum orçamento enviado ainda</p>
<p className="text-sm">Os orçamentos aparecem aqui assim que forem enviados ao cliente.</p>
</div>
)}
>
{(o) => (
<Row id={o.id}>
<Cell>{o.cliente}</Cell>
<Cell>{o.servico}</Cell>
<Cell className="text-right">{o.valor}</Cell>
</Row>
)}
</TableBody>
</Table>
);
}Busca, paginação e status
Para uma tabela completa com busca, paginação e badges de status, use o bloco Data Table.
Quando usar
- Use para dados tabulares que o usuário compara coluna a coluna: pedidos, faturas, leads, relatórios.
- Não use para layout de página ou para listas de um único dado por linha — prefira
Grid List. - Não use para escolher opções em formulários — use
Select,ListBoxouCheckbox. - Para listas longas, combine com
Paginationou use o bloco Data Table. - Alinhe números à direita com
className="text-right font-mono tabular-nums"noColumne noCell.
API
Table
Aceita todas as props do Table do React Aria. As mais usadas:
Prop
Type
Column
Aceita todas as props do Column do React Aria.
Prop
Type
TableBody
Aceita todas as props do TableBody do React Aria.
Prop
Type
Row e Cell
Row aceita as props do Row do React Aria — id é obrigatório para seleção;
href transforma a linha em link. Cell aceita as props do Cell do React Aria e className para alinhamento.