HextaUI

Table

Eine responsive Tabelle mit Flächenstil, umbrechenden oder kompakten Zellen, fixierten Kopfzeilen, angehefteten Spalten und Scroll-Hinweisen.

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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

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>

Nutze <TableHead scope="row"> für die Zelle, die jede Zeile benennt. Sie wird für Screenreader als Header der Zeile gelesen und erhält eine stärkere Textfarbe. Für Sortieren, Filtern und Auswahl baue auf <DataTable /> auf.

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

Standard

Die Standardvariante hat keine Fläche, nur feine Linien zwischen den Zeilen, für Tabellen in einer Karte oder einem Seitenabschnitt. <TableCaption /> beschreibt die Tabelle darunter.

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

Kompakt

size="sm" verringert Zeilenhöhe und Padding für dichte Daten.

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

Umbrechender Text

Zellen bleiben standardmäßig in einer Zeile, was zu Daten passt. wrap lässt sie umbrechen und richtet Zeilen oben aus, für Fließtext wie Glossare oder API-Referenzen. Kombiniere es mit table-fixed und Spaltenbreiten.

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

Breite Tabellen

Tabellen, die breiter als ihr Container sind, scrollen seitlich. Weiche Fades zeigen, dass es auf beiden Seiten mehr gibt, und der Scrollbereich wird fokussierbar, sodass Tastaturnutzer ihn mit den Pfeiltasten scrollen können. Schalte die Fades mit scrollFade={false} ab.

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

Fixierte Spalte

pinned="start" hält die erste Spalte im Blick, während der Rest scrollt, und pinnedEdge zeigt einen Schatten, sobald Inhalt darunter gleitet.

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 hält den Header in einer höhenbegrenzten Tabelle im Blick und fügt eine feine Linie darunter hinzu, sobald Zeilen darunter scrollen.

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

Rechts nach links

Ausrichtung, fixierte Spalten und Scroll-Fades folgen der Leserichtung.

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>
  )
}
TasteAktion
TabFokussiert den Scrollbereich, wenn die Tabelle überläuft, danach die Links und Buttons darin.
←→Scrollt eine fokussierte, überlaufende Tabelle seitlich.
↑↓Scrollt eine fokussierte Tabelle mit Sticky-Header nach oben und unten.
  • Die Teile rendern native Tabellenelemente, sodass Screenreader Zeilen, Spalten und Header ohne zusätzliche Rollen ansagen.
  • Markiere die Zelle, die eine Zeile benennt, mit <TableHead scope="row">, damit jeder Wert mit seiner Zeile und Spalte vorgelesen wird.
  • Der Scrollbereich gehört nur zur Tab-Reihenfolge, solange er überläuft, sodass Tabellen, die passen, keinen zusätzlichen Stopp erzeugen.

Jeder Teil rendert sein natives Element und akzeptiert dessen Attribute.

PropTypStandard
variantsurface fügt einen abgerundeten feinen Rahmen und ein gedämpftes Header-Band hinzu.
"default" | "surface""default"
size
"sm" | "default""default"
wrapLass Zellentext umbrechen und richte Zeilen oben aus.
booleanfalse
scrollFadeBlende die Ränder aus, hinter denen es mehr zu scrollen gibt. Entfällt, wenn Spalten fixiert sind.
booleantrue
stickyHeaderHalte den Header im Blick. Gib dem Container über containerClassName eine maximale Höhe.
booleanfalse
containerClassNameKlassen für den Scroll-Container.
string–
containerRefRef auf den Scroll-Container.
Ref<HTMLDivElement>–
AttributBeschreibung
data-slot="table-frame"Der äußere Rahmen, mit data-variant, data-size und data-wrap.
data-slot="table-container"Der Scroll-Container.
data-overflowingVorhanden am Container, solange sein Inhalt überläuft. Er ist dann fokussierbar.
data-scrolled-start / data-scrolled-endVorhanden, solange Inhalt vor oder nach dem sichtbaren Bereich verborgen ist.
data-scrolled-topVorhanden, sobald eine Tabelle mit Sticky-Header gescrollt wurde.
--table-bgZeilenhintergrund. Folgt einer umgebenden Karte oder einem Popover.
--table-head-bgHintergrund des Header-Bands in der Surface-Variante.
--table-cell-px / --table-cell-pyZellen-Padding. Über size gesetzt.
PropTypStandard
scopeNutze row für die Zelle, die eine Zeile benennt. Im Body erhält sie Zellen-Padding und Vordergrundtext.
"col" | "row"–
alignend schaltet außerdem auf tabellarische Ziffern um, damit Ziffern fluchten.
"start" | "center" | "end""start"
pinnedHalte die Spalte im Blick, während die Tabelle seitlich scrollt. Setze denselben Wert an ihrem Header und jeder Zelle.
"start" | "end"–
pinnedEdgeZeichne einen weichen Schatten am inneren Rand der letzten fixierten Spalte, solange Inhalt dahinter verborgen ist.
booleanfalse
AttributBeschreibung
data-slot="table-head"Header-Zellen per CSS ansprechen.
data-alignDie Ausrichtung.
data-pinnedDie fixierte Seite.
PropTypStandard
alignend schaltet außerdem auf tabellarische Ziffern um, damit Ziffern fluchten.
"start" | "center" | "end""start"
pinnedHalte die Spalte im Blick, während die Tabelle seitlich scrollt. Setze denselben Wert an ihrem Header und jeder Zelle.
"start" | "end"–
pinnedEdgeZeichne einen weichen Schatten am inneren Rand der letzten fixierten Spalte, solange Inhalt dahinter verborgen ist.
booleanfalse
AttributBeschreibung
data-slot="table-cell"Zellen per CSS ansprechen.
data-alignDie Ausrichtung.
data-pinnedDie fixierte Seite.
AttributBeschreibung
data-slot="table-row"Zeilen per CSS ansprechen.
data-state="selected"Setze es, um eine ausgewählte Zeile hervorzuheben.
AttributBeschreibung
data-slot="table-header"Das thead.
data-slot="table-body"Das tbody.
data-slot="table-footer"Das tfoot, in der Farbe des Header-Bands.
data-slot="table-caption"Unter der Tabelle. In der Surface-Variante sitzt sie im Rahmen.

In Blocks verwendet

Blocks, die auf Table aufbauen.