wui
组件

表格 Table

用原生表格语义组织和比较结构化数据。

基础示例

Loading…
pnpm dlx wui@latest add @wui/table
components/ui/table.tsx
import * 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 用于展示具有稳定列结构、需要横向比较的数据。组件保留原生表格元素的语义,适合账单、成员列表、用量明细和配置清单。小屏幕下外层容器会横向滚动,避免压缩内容或破坏列关系。

如果内容主要用于页面布局、只有单列键值,或需要虚拟滚动和复杂数据处理,应选择更合适的布局或数据网格方案。

组件属性

PropTypeDefaultDescription
containerClassNamestringExtra classes applied to the horizontal overflow container.
density"default" | "compact"defaultControls cell padding and row height.
stripedbooleanfalseAdds a subtle background to alternating body rows.
stickyHeaderbooleanfalseKeeps the header visible while the table container scrolls.

TableHeaderTableBodyTableFooterTableRowTableHeadTableCellTableCaption 接受对应原生 HTML 元素的全部属性。

事件

所有原生表格事件都会透传,例如在 TableRow 上使用 onClickonPointerEnter 或键盘事件。组件不内置选择逻辑;需要展示选中状态时,在行上设置 data-state="selected",并由业务状态负责更新。

拓展使用

紧凑密度与斑马纹

density="compact" 减少单元格留白,适合信息密度较高的后台页面;striped 为相邻数据行添加轻微层次。TableFooter 可承载合计或汇总信息。

Loading…

使用 className 控制表格本身,使用 containerClassName 定制横向滚动容器。数值列建议右对齐并添加 tabular-nums,以便快速比较。

高级数据表格

基础组件保持原生表格语义,筛选、排序、展开和拖动等业务状态由使用方组合。下面的完整示例同时展示级联表头、树形数据、同级拖动排序、列内筛选、排序、行选择、紧凑密度、固定表头和左右固定列。

Loading…

stickyHeader 会让表头在 containerClassName 设置的滚动区域内保持可见。TableHeadTableCellpinned 支持固定到 leftright,横向滚动时仍保留在容器边缘。连续固定多列时通过 pinOffset 指定相对边缘的累计距离;建议使用 colgroup 锁定列宽,并让表头与单元格使用相同的 pinOffset

级联表头继续使用原生 colSpanrowSpan。树形展开、筛选、选择和拖动排序没有绑定特定状态库,示例中的状态可以替换为 TanStack Table、服务端查询或业务自己的数据模型。