HextaUI

Table

面のスタイル、折り返しまたはコンパクトなセル、固定ヘッダー、固定列、スクロールのヒントを備えたレスポンシブなテーブル。

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

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

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>

各行に名前を付けるセルには <TableHead scope="row"> を使います。スクリーンリーダーには行のヘッダーとして読まれ、テキストの色も濃くなります。並べ替え、フィルター、選択は <DataTable /> の上に構築してください。

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

デフォルト

デフォルトのバリアントには面がなく、行の間に細い線があるだけで、card やページのセクションの内側に置くテーブル向けです。<TableCaption /> はその下のテーブルを説明します。

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

コンパクト

size="sm" は、密なデータ向けに行の高さとパディングを詰めます。

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

テキストの折り返し

セルはデフォルトで1行に収まり、データ向きです。wrap はセルの折り返しを許可して行を上揃えにし、用語集や API リファレンスのような文章向けです。table-fixed と列幅を組み合わせます。

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

幅の広いテーブル

コンテナーより幅の広いテーブルは横にスクロールします。柔らかいフェードがどちらかの側にさらにあることを示し、スクロール領域はフォーカス可能になるので、キーボードユーザーは矢印キーでスクロールできます。フェードは 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>
  )
}

固定列

pinned="start" は、残りがスクロールしても最初の列を表示したままにし、pinnedEdge はコンテンツがその下に潜り込むと影を表示します。

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 は、高さが制限されたテーブルでヘッダーを表示したままにし、行がその下をスクロールすると細い線を加えます。

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

右から左

配置、固定列、スクロールのフェードは文字の読み方向に従います。

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>
  )
}
キーアクション
Tabテーブルがあふれているとき、スクロール領域にフォーカスし、その後にその内側のリンクとボタンへ移ります。
←→フォーカスされた、あふれているテーブルを横にスクロールします。
↑↓sticky ヘッダー付きのフォーカスされたテーブルを上下にスクロールします。
  • 各パーツはネイティブのテーブル要素を描画するため、スクリーンリーダーは追加の role なしで行、列、ヘッダーを読み上げます。
  • 行に名前を付けるセルを <TableHead scope="row"> でマークすると、各値がその行と列とともに読み上げられます。
  • スクロール領域は、あふれているときだけタブ順に入るので、収まっているテーブルによってタブストップが増えることはありません。

すべてのパーツはネイティブ要素を描画し、その属性を受け付けます。

プロパティ型デフォルト
variantsurface は、角丸の細い枠と、淡いヘッダー帯を追加します。
"default" | "surface""default"
size
"sm" | "default""default"
wrapセルのテキストを折り返し、行を上揃えにします。
booleanfalse
scrollFadeスクロール先にさらにコンテンツがある端をフェードします。列を固定している場合はスキップされます。
booleantrue
stickyHeaderヘッダーを表示したままにします。containerClassName でコンテナーに最大の高さを指定してください。
booleanfalse
containerClassNameスクロールコンテナー用のクラス。
string–
containerRefスクロールコンテナーへの ref。
Ref<HTMLDivElement>–
属性説明
data-slot="table-frame"外側の枠。data-variant、data-size、data-wrap を持ちます。
data-slot="table-container"スクロールコンテナ。
data-overflowingコンテンツがあふれているとき、コンテナーに付きます。そのときはフォーカス可能です。
data-scrolled-start / data-scrolled-end表示領域の前後にコンテンツが隠れている間付きます。
data-scrolled-topsticky ヘッダーのテーブルがスクロールされると付きます。
--table-bg行の背景。周囲の card や popover に従います。
--table-head-bgsurface バリアントのヘッダー帯の背景。
--table-cell-px / --table-cell-pyセルのパディング。size によって設定されます。
プロパティ型デフォルト
scope行に名前を付けるセルには row を使います。本体の内側では、セルのパディングと前景色のテキストが適用されます。
"col" | "row"–
alignend は等幅数字にも切り替わり、桁が揃います。
"start" | "center" | "end""start"
pinnedテーブルが横にスクロールしても、その列を表示したままにします。ヘッダーとすべてのセルに同じ値を設定してください。
"start" | "end"–
pinnedEdge固定された最後の列の裏にコンテンツが隠れている間、その内側の端に柔らかい影を描画します。
booleanfalse
属性説明
data-slot="table-head"CSS でヘッダーセルを指定します。
data-align配置。
data-pinned固定される側。
プロパティ型デフォルト
alignend は等幅数字にも切り替わり、桁が揃います。
"start" | "center" | "end""start"
pinnedテーブルが横にスクロールしても、その列を表示したままにします。ヘッダーとすべてのセルに同じ値を設定してください。
"start" | "end"–
pinnedEdge固定された最後の列の裏にコンテンツが隠れている間、その内側の端に柔らかい影を描画します。
booleanfalse
属性説明
data-slot="table-cell"CSS でセルを指定します。
data-align配置。
data-pinned固定される側。
属性説明
data-slot="table-row"CSS で行を指定します。
data-state="selected"選択された行をハイライトするように設定します。
属性説明
data-slot="table-header"thead。
data-slot="table-body"tbody。
data-slot="table-footer"tfoot。ヘッダー帯の色になります。
data-slot="table-caption"テーブルの下。surface バリアントでは枠の内側に収まります。

使用しているブロック

Table の上に構築されるブロック。