表格 Table
用原生表格语义组织和比较结构化数据。
基础示例
pnpm dlx wui@latest add @wui/tableimport * as React from "react"
import { cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const tableVariants = cva("w-full caption-bottom text-sm", {
variants: {
density: {
default: "",
compact:
"[&_[data-slot=table-cell]]:px-3 [&_[data-slot=table-cell]]:py-2 [&_[data-slot=table-head]]:h-9 [&_[data-slot=table-head]]:px-3",
},
striped: {
true: "[&_[data-slot=table-body]_[data-slot=table-row]:nth-child(even)]:bg-muted/35",
false: "",
},
},
defaultVariants: {
density: "default",
striped: false,
},
})
export interface TableProps
extends React.ComponentProps<"table"> {
/** Extra classes applied to the horizontal overflow container. */
containerClassName?: string
/** Controls cell padding and row height. @default "default" */
density?: "default" | "compact"
/** Adds a subtle background to alternating body rows. @default false */
striped?: boolean
/** Keeps the header visible while the table container scrolls. @default false */
stickyHeader?: boolean
}
/** A responsive native table with composable semantic sections. */
function Table({
className,
containerClassName,
density = "default",
striped = false,
stickyHeader = false,
...props
}: TableProps) {
return (
<div
data-slot="table-container"
className={cn(
"relative isolate w-full overflow-auto overscroll-contain",
containerClassName
)}
>
<table
data-slot="table"
data-density={density}
className={cn(
tableVariants({ density, striped }),
stickyHeader &&
"[&_[data-slot=table-header]]:sticky [&_[data-slot=table-header]]:top-0 [&_[data-slot=table-header]]:z-20 [&_[data-slot=table-header]]:bg-background",
className
)}
{...props}
/>
</div>
)
}
function TableHeader({
className,
...props
}: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({
className,
...props
}: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({
className,
...props
}: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({
className,
...props
}: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"group/row border-b transition-colors hover:bg-muted/50 data-[dragging=true]:opacity-45 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
)
}
export interface TableHeadProps extends React.ComponentProps<"th"> {
/** Pins the column to an edge of the horizontal scroll container. */
pinned?: "left" | "right"
/** Distance from the pinned edge, useful when pinning multiple columns. @default 0 */
pinOffset?: number | string
}
function TableHead({
className,
pinned,
pinOffset = 0,
style,
...props
}: TableHeadProps) {
return (
<th
data-slot="table-head"
data-pinned={pinned}
className={cn(
"h-10 whitespace-nowrap px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
pinned &&
"z-30 bg-background group-hover/row:bg-muted/50 group-data-[state=selected]/row:bg-muted",
pinned === "left" && "border-r",
pinned === "right" && "border-l",
className
)}
style={{
...style,
...(pinned ? { position: "sticky" } : {}),
...(pinned === "left"
? { left: pinOffset, insetInlineStart: pinOffset }
: {}),
...(pinned === "right"
? { right: pinOffset, insetInlineEnd: pinOffset }
: {}),
}}
{...props}
/>
)
}
export interface TableCellProps extends React.ComponentProps<"td"> {
/** Pins the column to an edge of the horizontal scroll container. */
pinned?: "left" | "right"
/** Distance from the pinned edge, useful when pinning multiple columns. @default 0 */
pinOffset?: number | string
}
function TableCell({
className,
pinned,
pinOffset = 0,
style,
...props
}: TableCellProps) {
return (
<td
data-slot="table-cell"
data-pinned={pinned}
className={cn(
"whitespace-nowrap p-4 align-middle [&:has([role=checkbox])]:pr-0",
pinned &&
"z-10 bg-background group-hover/row:bg-muted/50 group-data-[state=selected]/row:bg-muted",
pinned === "left" && "border-r",
pinned === "right" && "border-l",
className
)}
style={{
...style,
...(pinned ? { position: "sticky" } : {}),
...(pinned === "left"
? { left: pinOffset, insetInlineStart: pinOffset }
: {}),
...(pinned === "right"
? { right: pinOffset, insetInlineEnd: pinOffset }
: {}),
}}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
tableVariants,
}
组件作用
Table 用于展示具有稳定列结构、需要横向比较的数据。组件保留原生表格元素的语义,适合账单、成员列表、用量明细和配置清单。小屏幕下外层容器会横向滚动,避免压缩内容或破坏列关系。
如果内容主要用于页面布局、只有单列键值,或需要虚拟滚动和复杂数据处理,应选择更合适的布局或数据网格方案。
组件属性
| Prop | Type | Default | Description |
|---|---|---|---|
| containerClassName | string | — | Extra classes applied to the horizontal overflow container. |
| density | "default" | "compact" | default | Controls cell padding and row height. |
| striped | boolean | false | Adds a subtle background to alternating body rows. |
| stickyHeader | boolean | false | Keeps the header visible while the table container scrolls. |
TableHeader、TableBody、TableFooter、TableRow、TableHead、TableCell 与 TableCaption 接受对应原生 HTML 元素的全部属性。
事件
所有原生表格事件都会透传,例如在 TableRow 上使用 onClick、onPointerEnter 或键盘事件。组件不内置选择逻辑;需要展示选中状态时,在行上设置 data-state="selected",并由业务状态负责更新。
拓展使用
紧凑密度与斑马纹
density="compact" 减少单元格留白,适合信息密度较高的后台页面;striped 为相邻数据行添加轻微层次。TableFooter 可承载合计或汇总信息。
使用 className 控制表格本身,使用 containerClassName 定制横向滚动容器。数值列建议右对齐并添加 tabular-nums,以便快速比较。
高级数据表格
基础组件保持原生表格语义,筛选、排序、展开和拖动等业务状态由使用方组合。下面的完整示例同时展示级联表头、树形数据、同级拖动排序、列内筛选、排序、行选择、紧凑密度、固定表头和左右固定列。
stickyHeader 会让表头在 containerClassName 设置的滚动区域内保持可见。TableHead 与 TableCell 的 pinned 支持固定到 left 或 right,横向滚动时仍保留在容器边缘。连续固定多列时通过 pinOffset 指定相对边缘的累计距离;建议使用 colgroup 锁定列宽,并让表头与单元格使用相同的 pinOffset。
级联表头继续使用原生 colSpan 与 rowSpan。树形展开、筛选、选择和拖动排序没有绑定特定状态库,示例中的状态可以替换为 TanStack Table、服务端查询或业务自己的数据模型。