HextaUI

Table

Un tableau responsive avec un style de surface, des cellules à retour à la ligne ou compactes, des en-têtes fixes, des colonnes épinglées et des indications de défilement.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

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>

Utilisez <TableHead scope="row"> pour la cellule qui nomme chaque ligne. Elle est lue comme l'en-tête de la ligne par les lecteurs d'écran et reçoit une couleur de texte plus marquée. Pour le tri, le filtrage et la sélection, appuyez-vous sur <DataTable />.

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

Par défaut

La variante par défaut n'a pas de surface, juste des filets entre les lignes, pour les tableaux placés dans une carte ou une section de page. <TableCaption /> décrit le tableau en dessous.

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

Compact

size="sm" resserre la hauteur des lignes et le padding pour les données denses.

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

Texte à la ligne

Les cellules restent sur une seule ligne par défaut, ce qui convient aux données. wrap les laisse passer à la ligne et aligne les lignes en haut, pour de la prose comme des glossaires ou des références d'API. Associez-le à table-fixed et à des largeurs de colonnes.

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

Tableaux larges

Les tableaux plus larges que leur conteneur défilent latéralement. De doux fondus montrent qu'il y a plus de chaque côté, et la zone de défilement devient focalisable pour que les utilisateurs du clavier la fassent défiler avec les flèches. Désactivez les fondus avec 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>
  )
}

Colonne épinglée

pinned="start" garde la première colonne visible pendant que le reste défile, et pinnedEdge affiche une ombre dès que du contenu glisse dessous.

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 garde l'en-tête visible dans un tableau à hauteur limitée, et ajoute un filet dessous dès que des lignes défilent derrière.

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 droite à gauche

L'alignement, les colonnes épinglées et les fondus de défilement suivent le sens de lecture.

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>
  )
}
ToucheAction
TabDonne le focus à la zone de défilement quand le tableau déborde, puis aux liens et boutons à l'intérieur.
←→Fait défiler latéralement un tableau focalisé qui déborde.
↑↓Fait défiler vers le haut et le bas un tableau focalisé à en-tête sticky.
  • Les parties rendent des éléments de tableau natifs : les lecteurs d'écran annoncent donc lignes, colonnes et en-têtes sans rôles supplémentaires.
  • Marquez la cellule qui nomme une ligne avec <TableHead scope="row"> pour que chaque valeur soit lue avec sa ligne et sa colonne.
  • La zone de défilement n'entre dans l'ordre de tabulation que lorsqu'elle déborde, si bien que les tableaux qui tiennent n'ajoutent pas d'arrêt superflu.

Chaque partie rend son élément natif et accepte ses attributs.

PropTypePar défaut
variantsurface ajoute un cadre arrondi en filet et une bande d'en-tête atténuée.
"default" | "surface""default"
size
"sm" | "default""default"
wrapLaisse le texte des cellules passer à la ligne et aligne les lignes en haut.
booleanfalse
scrollFadeEstompe les bords qui ont plus de contenu à faire défiler. Ignoré quand des colonnes sont épinglées.
booleantrue
stickyHeaderGarde l'en-tête visible. Donnez au conteneur une hauteur maximale via containerClassName.
booleanfalse
containerClassNameClasses pour le conteneur de défilement.
string–
containerRefRef vers le conteneur de défilement.
Ref<HTMLDivElement>–
AttributDescription
data-slot="table-frame"Le cadre extérieur, avec data-variant, data-size et data-wrap.
data-slot="table-container"Le conteneur de défilement.
data-overflowingPrésent sur le conteneur tant que son contenu déborde. Il est alors focalisable.
data-scrolled-start / data-scrolled-endPrésent tant que du contenu est caché avant ou après la zone visible.
data-scrolled-topPrésent une fois qu'un tableau à en-tête sticky a défilé.
--table-bgArrière-plan de la ligne. Suit une carte ou un popover englobant.
--table-head-bgArrière-plan de la bande d'en-tête dans la variante surface.
--table-cell-px / --table-cell-pyPadding des cellules. Défini par size.
PropTypePar défaut
scopeUtilisez row pour la cellule qui nomme une ligne. Dans le corps, elle reçoit le padding des cellules et le texte de premier plan.
"col" | "row"–
alignend passe aussi aux chiffres tabulaires pour aligner les chiffres.
"start" | "center" | "end""start"
pinnedGarde la colonne visible pendant le défilement latéral du tableau. Définissez la même valeur sur son en-tête et sur chaque cellule.
"start" | "end"–
pinnedEdgeTrace une ombre douce sur le bord intérieur de la dernière colonne épinglée tant que du contenu est caché derrière.
booleanfalse
AttributDescription
data-slot="table-head"Cible les cellules d'en-tête en CSS.
data-alignL'alignement.
data-pinnedLe côté épinglé.
PropTypePar défaut
alignend passe aussi aux chiffres tabulaires pour aligner les chiffres.
"start" | "center" | "end""start"
pinnedGarde la colonne visible pendant le défilement latéral du tableau. Définissez la même valeur sur son en-tête et sur chaque cellule.
"start" | "end"–
pinnedEdgeTrace une ombre douce sur le bord intérieur de la dernière colonne épinglée tant que du contenu est caché derrière.
booleanfalse
AttributDescription
data-slot="table-cell"Cible les cellules en CSS.
data-alignL'alignement.
data-pinnedLe côté épinglé.
AttributDescription
data-slot="table-row"Cible les lignes en CSS.
data-state="selected"Définissez-le pour mettre en évidence une ligne sélectionnée.
AttributDescription
data-slot="table-header"Le thead.
data-slot="table-body"Le tbody.
data-slot="table-footer"Le tfoot, sur la couleur de la bande d'en-tête.
data-slot="table-caption"Sous le tableau. Dans la variante surface, il se place à l'intérieur du cadre.

Utilisé dans les blocks

Des blocks qui s’appuient sur Table.