HextaUI

Table

Una tabla responsive con estilo de superficie, celdas que se ajustan o compactas, encabezados fijos, columnas ancladas e indicadores de desplazamiento.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que 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>

Usa <TableHead scope="row"> para la celda que da nombre a cada fila. Los lectores de pantalla la leen como el encabezado de la fila y recibe un color de texto más marcado. Para ordenar, filtrar y seleccionar, construye sobre <DataTable />.

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

Predeterminado

La variante por defecto no tiene superficie, solo líneas finas entre filas, para tablas que van dentro de una tarjeta o una sección de página. <TableCaption /> describe la tabla que tiene debajo.

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" reduce la altura de las filas y el relleno para datos 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 con ajuste

Las celdas permanecen en una sola línea por defecto, lo que encaja con los datos. wrap permite que se ajusten y alinea las filas arriba, para prosa como glosarios o referencias de API. Combínalo con table-fixed y anchos de columna.

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>
  )
}

Tablas anchas

Las tablas más anchas que su contenedor se desplazan lateralmente. Unos degradados suaves indican que hay más a cada lado, y el área de scroll se vuelve enfocable para que los usuarios de teclado puedan desplazarla con las flechas. Desactiva los degradados con 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>
  )
}

Columna fijada

pinned="start" mantiene la primera columna a la vista mientras el resto se desplaza, y pinnedEdge muestra una sombra cuando el contenido se desliza por debajo.

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 mantiene el encabezado a la vista en una tabla de altura limitada, y añade una línea fina debajo cuando las filas se desplazan por debajo.

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>
  )
}

De derecha a izquierda

La alineación, las columnas fijadas y los degradados de scroll siguen la dirección de lectura.

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>
  )
}
KeyAcción
TabEnfoca el área de scroll cuando la tabla se desborda, y luego los enlaces y botones de su interior.
←→Desplaza lateralmente una tabla enfocada que se desborda.
↑↓Desplaza hacia arriba y abajo una tabla enfocada con encabezado sticky.
  • Las partes renderizan elementos de tabla nativos, así que los lectores de pantalla anuncian filas, columnas y encabezados sin roles adicionales.
  • Marca la celda que da nombre a una fila con <TableHead scope="row"> para que cada valor se lea junto con su fila y su columna.
  • El área de scroll solo entra en el orden de tabulación mientras se desborda, así que las tablas que caben no añaden una parada extra.

Cada parte renderiza su elemento nativo y acepta sus atributos.

PropTipoPredeterminado
variantsurface añade un marco redondeado de línea fina y una banda de encabezado atenuada.
"default" | "surface""default"
size
"sm" | "default""default"
wrapPermite que el texto de las celdas se ajuste y alinea las filas arriba.
booleanfalse
scrollFadeDesvanece los bordes que tienen más contenido por desplazar. Se omite cuando hay columnas fijadas.
booleantrue
stickyHeaderMantiene el encabezado a la vista. Dale al contenedor una altura máxima mediante containerClassName.
booleanfalse
containerClassNameClases para el contenedor de scroll.
string–
containerRefRef al contenedor de scroll.
Ref<HTMLDivElement>–
AtributoDescripción
data-slot="table-frame"El marco exterior, con data-variant, data-size y data-wrap.
data-slot="table-container"El contenedor de desplazamiento.
data-overflowingPresente en el contenedor mientras su contenido se desborda. Entonces es enfocable.
data-scrolled-start / data-scrolled-endPresente mientras hay contenido oculto antes o después del área visible.
data-scrolled-topPresente una vez que una tabla con encabezado sticky ha hecho scroll.
--table-bgFondo de la fila. Sigue al card o popover que la rodea.
--table-head-bgFondo de la banda del encabezado en la variante surface.
--table-cell-px / --table-cell-pyRelleno de las celdas. Definido por size.
PropTipoPredeterminado
scopeUsa row para la celda que da nombre a una fila. Dentro del cuerpo recibe el relleno de celda y el texto de primer plano.
"col" | "row"–
alignend también cambia a números tabulares para que los dígitos se alineen.
"start" | "center" | "end""start"
pinnedMantiene la columna a la vista mientras la tabla se desplaza lateralmente. Define el mismo valor en su encabezado y en todas sus celdas.
"start" | "end"–
pinnedEdgeDibuja una sombra suave en el borde interior de la última columna fijada mientras hay contenido oculto detrás.
booleanfalse
AtributoDescripción
data-slot="table-head"Apunta a las celdas de encabezado en CSS.
data-alignLa alineación.
data-pinnedEl lado fijado.
PropTipoPredeterminado
alignend también cambia a números tabulares para que los dígitos se alineen.
"start" | "center" | "end""start"
pinnedMantiene la columna a la vista mientras la tabla se desplaza lateralmente. Define el mismo valor en su encabezado y en todas sus celdas.
"start" | "end"–
pinnedEdgeDibuja una sombra suave en el borde interior de la última columna fijada mientras hay contenido oculto detrás.
booleanfalse
AtributoDescripción
data-slot="table-cell"Apunta a las celdas en CSS.
data-alignLa alineación.
data-pinnedEl lado fijado.
AtributoDescripción
data-slot="table-row"Apunta a las filas en CSS.
data-state="selected"Defínelo para resaltar una fila seleccionada.
AtributoDescripción
data-slot="table-header"El thead.
data-slot="table-body"El tbody.
data-slot="table-footer"El tfoot, con el color de la banda del encabezado.
data-slot="table-caption"Debajo de la tabla. En la variante surface queda dentro del marco.

Usado en bloques

Bloques que se construyen sobre Table.