HextaUI

Table

Uma tabela responsiva com estilo de superfície, células com quebra de linha ou compactas, cabeçalhos fixos, colunas fixadas e indicações de rolagem.

InvoiceClientStatusAmount
INV-0418NorthwindPaid$2,400.00
INV-0419GlobexPending$860.00
INV-0420InitechPaid$1,320.50
INV-0421UmbrellaOverdue$415.00
Total$4,995.50
import { Badge, BadgeDot } from "@/components/ui/badge"
import {
  Table,
  TableBody,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const invoices = [
  { id: "INV-0418", client: "Northwind", status: "Paid", amount: 2400 },
  { id: "INV-0419", client: "Globex", status: "Pending", amount: 860 },
  { id: "INV-0420", client: "Initech", status: "Paid", amount: 1320.5 },
  { id: "INV-0421", client: "Umbrella", status: "Overdue", amount: 415 },
]

const currency = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
})

const tones = {
  Paid: "success",
  Pending: "info",
  Overdue: "destructive",
} as const

export function TableDemo() {
  const total = invoices.reduce((sum, invoice) => sum + invoice.amount, 0)

  return (
    <Table variant="surface" className="min-w-md">
      <TableHeader>
        <TableRow>
          <TableHead>Invoice</TableHead>
          <TableHead>Client</TableHead>
          <TableHead>Status</TableHead>
          <TableHead align="end">Amount</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {invoices.map((invoice) => (
          <TableRow key={invoice.id}>
            <TableHead scope="row">{invoice.id}</TableHead>
            <TableCell>{invoice.client}</TableCell>
            <TableCell>
              <Badge variant={tones[invoice.status as keyof typeof tones]}>
                <BadgeDot />
                {invoice.status}
              </Badge>
            </TableCell>
            <TableCell align="end">{currency.format(invoice.amount)}</TableCell>
          </TableRow>
        ))}
      </TableBody>
      <TableFooter>
        <TableRow>
          <TableCell colSpan={3}>Total</TableCell>
          <TableCell align="end">{currency.format(total)}</TableCell>
        </TableRow>
      </TableFooter>
    </Table>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/table.json

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
<Table variant="surface">
  <TableHeader>
    <TableRow>
      <TableHead>Invoice</TableHead>
      <TableHead align="end">Amount</TableHead>
    </TableRow>
  </TableHeader>
  <TableBody>
    <TableRow>
      <TableHead scope="row">INV-0418</TableHead>
      <TableCell align="end">$2,400.00</TableCell>
    </TableRow>
  </TableBody>
</Table>

Use <TableHead scope="row"> para a célula que nomeia cada linha. Ela é lida como o cabeçalho da linha pelos leitores de tela e recebe uma cor de texto mais forte. Para ordenação, filtragem e seleção, construa sobre <DataTable />.

Table
├── TableCaption
├── TableHeader
│   └── TableRow
│       └── TableHead
├── TableBody
│   └── TableRow
│       └── TableCell
└── TableFooter
    └── TableRow
        └── TableCell

Padrão

A variante padrão não tem superfície, apenas linhas finas entre as linhas, para tabelas dentro de um card ou de uma seção da página. <TableCaption /> descreve a tabela abaixo dele.

import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const releases = [
  { version: "2.4.0", date: "Oct 2, 2026", changes: 18 },
  { version: "2.3.1", date: "Sep 21, 2026", changes: 4 },
  { version: "2.3.0", date: "Sep 12, 2026", changes: 23 },
]

export function TablePlain() {
  return (
    <Table>
      <TableCaption>Recent releases.</TableCaption>
      <TableHeader>
        <TableRow>
          <TableHead>Version</TableHead>
          <TableHead>Released</TableHead>
          <TableHead align="end">Changes</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {releases.map((release) => (
          <TableRow key={release.version}>
            <TableHead scope="row">{release.version}</TableHead>
            <TableCell>{release.date}</TableCell>
            <TableCell align="end">{release.changes}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Compacto

size="sm" reduz a altura da linha e o padding para dados densos.

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const rows = [
  { region: "us-east-1", latency: "42 ms", uptime: "99.99%" },
  { region: "eu-west-2", latency: "38 ms", uptime: "99.98%" },
  { region: "ap-south-1", latency: "71 ms", uptime: "99.95%" },
]

export function TableSizes() {
  return (
    <Table variant="surface" size="sm">
      <TableHeader>
        <TableRow>
          <TableHead>Region</TableHead>
          <TableHead align="end">Latency</TableHead>
          <TableHead align="end">Uptime</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {rows.map((row) => (
          <TableRow key={row.region}>
            <TableCell>
              <span className="font-mono text-xs">{row.region}</span>
            </TableCell>
            <TableCell align="end">{row.latency}</TableCell>
            <TableCell align="end">{row.uptime}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Texto com quebra de linha

As células ficam em uma linha por padrão, o que serve para dados. wrap permite que quebrem e alinha as linhas ao topo, para prosa como glossários ou referências de API. Combine com table-fixed e larguras de coluna.

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const terms = [
  {
    term: "Concentric radius",
    meaning:
      "An inner corner radius equal to the outer radius minus the gap between the two edges, so nested shapes look like they belong together.",
  },
  {
    term: "Hairline",
    meaning:
      "The thinnest line the screen can draw: 1px on standard displays and two thirds of a pixel on high-density ones.",
  },
]

export function TableWrap() {
  return (
    <Table variant="surface" wrap className="table-fixed">
      <TableHeader>
        <TableRow>
          <TableHead className="w-1/3">Term</TableHead>
          <TableHead>Meaning</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {terms.map((row) => (
          <TableRow key={row.term}>
            <TableHead scope="row">{row.term}</TableHead>
            <TableCell>
              <span className="text-muted-foreground">{row.meaning}</span>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Tabelas largas

Tabelas mais largas que o contêiner rolam para o lado. Esmaecimentos suaves mostram que há mais de cada lado, e a área de rolagem fica focável para que usuários de teclado possam rolá-la com as setas. Desative os esmaecimentos com scrollFade={false}.

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const months = [
  "Jan",
  "Feb",
  "Mar",
  "Apr",
  "May",
  "Jun",
  "Jul",
  "Aug",
  "Sep",
  "Oct",
  "Nov",
  "Dec",
]

const series = [
  {
    name: "Visitors",
    values: [
      812, 904, 1021, 990, 1203, 1340, 1288, 1410, 1502, 1622, 1590, 1711,
    ],
  },
  {
    name: "Sign-ups",
    values: [41, 52, 66, 59, 71, 83, 80, 92, 97, 110, 104, 121],
  },
]

export function TableScroll() {
  return (
    <Table variant="surface" className="w-max min-w-full">
      <TableHeader>
        <TableRow>
          <TableHead>Metric</TableHead>
          {months.map((month) => (
            <TableHead key={month} align="end">
              {month}
            </TableHead>
          ))}
        </TableRow>
      </TableHeader>
      <TableBody>
        {series.map((row) => (
          <TableRow key={row.name}>
            <TableHead scope="row">{row.name}</TableHead>
            {row.values.map((value, index) => (
              <TableCell key={months[index]} align="end">
                {value.toLocaleString("en-US")}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Coluna fixa

pinned="start" mantém a primeira coluna à vista enquanto o resto rola, e pinnedEdge mostra uma sombra quando o conteúdo desliza por baixo dela.

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const quarters = [
  "Q1 2025",
  "Q2 2025",
  "Q3 2025",
  "Q4 2025",
  "Q1 2026",
  "Q2 2026",
  "Q3 2026",
]

const teams = [
  { name: "Design systems", values: [12, 14, 15, 18, 21, 22, 24] },
  { name: "Platform", values: [31, 30, 33, 35, 34, 38, 40] },
  { name: "Growth", values: [8, 9, 11, 10, 12, 15, 16] },
]

export function TablePinned() {
  return (
    <Table variant="surface" className="w-max min-w-full">
      <TableHeader>
        <TableRow>
          <TableHead pinned="start" pinnedEdge>
            Team
          </TableHead>
          {quarters.map((quarter) => (
            <TableHead key={quarter} align="end">
              {quarter}
            </TableHead>
          ))}
        </TableRow>
      </TableHeader>
      <TableBody>
        {teams.map((team) => (
          <TableRow key={team.name}>
            <TableHead scope="row" pinned="start" pinnedEdge>
              {team.name}
            </TableHead>
            {team.values.map((value, index) => (
              <TableCell key={quarters[index]} align="end">
                {value}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

stickyHeader mantém o cabeçalho à vista em uma tabela de altura limitada e adiciona uma linha fina sob ele quando as linhas rolam por baixo.

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const events = Array.from({ length: 24 }, (_, index) => ({
  id: `evt_${(1024 + index).toString(16)}`,
  type: ["deploy", "rollback", "scale", "restart"][index % 4],
  minutes: index * 7 + 2,
}))

export function TableSticky() {
  return (
    <Table variant="surface" stickyHeader containerClassName="max-h-72">
      <TableHeader>
        <TableRow>
          <TableHead>Event</TableHead>
          <TableHead>Type</TableHead>
          <TableHead align="end">When</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {events.map((event) => (
          <TableRow key={event.id}>
            <TableCell>
              <span className="font-mono text-xs">{event.id}</span>
            </TableCell>
            <TableCell>{event.type}</TableCell>
            <TableCell align="end">{event.minutes} min ago</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}

Da direita para a esquerda

Alinhamento, colunas fixas e esmaecimentos de rolagem seguem a direção de leitura.

import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

const rows = [
  { name: "التصميم", members: 8, budget: "١٢٬٠٠٠" },
  { name: "الهندسة", members: 21, budget: "٤٨٬٥٠٠" },
]

export function TableRtl() {
  return (
    <div dir="rtl" className="w-full">
      <Table variant="surface">
        <TableHeader>
          <TableRow>
            <TableHead>الفريق</TableHead>
            <TableHead align="end">الأعضاء</TableHead>
            <TableHead align="end">الميزانية</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.name}>
              <TableHead scope="row">{row.name}</TableHead>
              <TableCell align="end">{row.members}</TableCell>
              <TableCell align="end">{row.budget}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  )
}
TeclaAção
TabFoca a área de rolagem quando a tabela transborda, depois os links e botões dentro dela.
←→Rola para o lado uma tabela com foco que transborda.
↑↓Rola para cima e para baixo uma tabela com foco e cabeçalho sticky.
  • As partes renderizam elementos nativos de tabela, então os leitores de tela anunciam linhas, colunas e cabeçalhos sem roles extras.
  • Marque a célula que nomeia uma linha com <TableHead scope="row"> para que cada valor seja lido junto com sua linha e coluna.
  • A área de rolagem só entra na ordem de tabulação enquanto transborda, então tabelas que cabem não adicionam uma parada extra.

Cada parte renderiza seu elemento nativo e aceita seus atributos.

PropTipoPadrão
variantsurface adiciona uma moldura de linha fina arredondada e uma faixa de cabeçalho atenuada.
"default" | "surface""default"
size
"sm" | "default""default"
wrapPermite que o texto das células quebre e alinha as linhas ao topo.
booleanfalse
scrollFadeEsmaece as bordas que têm mais conteúdo para rolar. Ignorado quando há colunas fixas.
booleantrue
stickyHeaderMantém o cabeçalho à vista. Dê ao contêiner uma altura máxima por containerClassName.
booleanfalse
containerClassNameClasses para o contêiner de rolagem.
string–
containerRefRef para o contêiner de rolagem.
Ref<HTMLDivElement>–
AtributoDescrição
data-slot="table-frame"A moldura externa, com data-variant, data-size e data-wrap.
data-slot="table-container"O contêiner de rolagem.
data-overflowingPresente no contêiner enquanto seu conteúdo transborda. Ele fica focável então.
data-scrolled-start / data-scrolled-endPresente enquanto há conteúdo oculto antes ou depois da área visível.
data-scrolled-topPresente depois que uma tabela com cabeçalho sticky rolou.
--table-bgFundo da linha. Acompanha um card ou popover ao redor.
--table-head-bgFundo da faixa do cabeçalho na variante surface.
--table-cell-px / --table-cell-pyPadding das células. Definido por size.
PropTipoPadrão
scopeUse row para a célula que nomeia uma linha. No corpo, recebe padding de célula e texto em primeiro plano.
"col" | "row"–
alignend também muda para números tabulares para os dígitos se alinharem.
"start" | "center" | "end""start"
pinnedMantém a coluna à vista enquanto a tabela rola de lado. Defina o mesmo valor no cabeçalho e em todas as células.
"start" | "end"–
pinnedEdgeDesenha uma sombra suave na borda interna da última coluna fixa enquanto há conteúdo escondido atrás dela.
booleanfalse
AtributoDescrição
data-slot="table-head"Seleciona as células de cabeçalho no CSS.
data-alignO alinhamento.
data-pinnedO lado fixado.
PropTipoPadrão
alignend também muda para números tabulares para os dígitos se alinharem.
"start" | "center" | "end""start"
pinnedMantém a coluna à vista enquanto a tabela rola de lado. Defina o mesmo valor no cabeçalho e em todas as células.
"start" | "end"–
pinnedEdgeDesenha uma sombra suave na borda interna da última coluna fixa enquanto há conteúdo escondido atrás dela.
booleanfalse
AtributoDescrição
data-slot="table-cell"Seleciona as células no CSS.
data-alignO alinhamento.
data-pinnedO lado fixado.
AtributoDescrição
data-slot="table-row"Seleciona as linhas no CSS.
data-state="selected"Defina para destacar uma linha selecionada.
AtributoDescrição
data-slot="table-header"O thead.
data-slot="table-body"O tbody.
data-slot="table-footer"O tfoot, na cor da faixa do cabeçalho.
data-slot="table-caption"Abaixo da tabela. Na variante surface, fica dentro da moldura.

Usado em blocos

Blocos que se baseiam em Table.