SubjectFromDate
Kumo v1.0.0 releasedVisal In5 seconds ago
New Job OfferCloudflare10 minutes ago
Daily Email DigestCloudflare1 hour ago
import { LayerCard, Table } from "@cloudflare/kumo";

export function TableBasicDemo() {
  return (
    <LayerCard className="p-0">
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.Head>Subject</Table.Head>
            <Table.Head>From</Table.Head>
            <Table.Head>Date</Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {emailData.slice(0, 3).map((row) => (
            <Table.Row key={row.id}>
              <Table.Cell>{row.subject}</Table.Cell>
              <Table.Cell>{row.from}</Table.Cell>
              <Table.Cell>{row.date}</Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

安装

桶式导出

import { Table } from "@cloudflare/kumo";

细粒度导入

import { Table } from "@cloudflare/kumo/components/table";

用法

import { Table, LayerCard } from "@cloudflare/kumo";

export default function Example() {
  return (
    <LayerCard className="p-0">
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.Head>Name</Table.Head>
            <Table.Head>Email</Table.Head>
            <Table.Head>Role</Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          <Table.Row>
            <Table.Cell>John Doe</Table.Cell>
            <Table.Cell>john@example.com</Table.Cell>
            <Table.Cell>Admin</Table.Cell>
          </Table.Row>
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

示例

带复选框

使用 Table.CheckHeadTable.CheckCell 添加行选择。两者都接受 onCheckedChange,其签名与底层 Checkbox 组件一致,并通过第二个参数暴露可选的事件详情。

较早的 onValueChange 属性仍可用,但已废弃 —— lint 规则会予以标记,并将在未来的 主版本中移除。迁移方式为重命名该属性:

// Before (deprecated)
<Table.CheckCell onValueChange={(checked) => toggleRow(id)} />

// After
<Table.CheckCell onCheckedChange={(checked) => toggleRow(id)} />

// With event details
<Table.CheckCell
  onCheckedChange={(checked, eventDetails) => {
    toggleRow(id);
    eventDetails?.event.stopPropagation();
  }}
/>
SubjectFromDate
Kumo v1.0.0 releasedVisal In5 seconds ago
New Job OfferCloudflare10 minutes ago
Daily Email DigestCloudflare1 hour ago
import { useState } from "react";
import { LayerCard, Table } from "@cloudflare/kumo";

export function TableWithCheckboxDemo() {
  const rows = emailData.slice(0, 3);
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());

  const toggleRow = (id: string) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  };

  const toggleAll = () => {
    if (selectedIds.size === rows.length) {
      setSelectedIds(new Set());
    } else {
      setSelectedIds(new Set(rows.map((r) => r.id)));
    }
  };

  return (
    <LayerCard className="p-0">
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.CheckHead
              checked={selectedIds.size === rows.length}
              indeterminate={
                selectedIds.size > 0 && selectedIds.size < rows.length
              }
              onCheckedChange={toggleAll}
              aria-label="Select all rows"
            />
            <Table.Head>Subject</Table.Head>
            <Table.Head>From</Table.Head>
            <Table.Head>Date</Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {rows.map((row) => (
            <Table.Row key={row.id}>
              <Table.CheckCell
                checked={selectedIds.has(row.id)}
                onCheckedChange={() => toggleRow(row.id)}
                aria-label={`Select ${row.subject}`}
              />
              <Table.Cell>{row.subject}</Table.Cell>
              <Table.Cell>{row.from}</Table.Cell>
              <Table.Cell>{row.date}</Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

紧凑表头

Table.Header 上使用 variant="compact" 可获得更紧凑的表头样式。

SubjectFromDate
Kumo v1.0.0 releasedVisal In5 seconds ago
New Job OfferCloudflare10 minutes ago
Daily Email DigestCloudflare1 hour ago
import { LayerCard, Table } from "@cloudflare/kumo";

export function TableWithCompactHeaderDemo() {
  return (
    <LayerCard className="p-0">
      <Table>
        <Table.Header variant="compact">
          <Table.Row>
            <Table.Head>Subject</Table.Head>
            <Table.Head>From</Table.Head>
            <Table.Head>Date</Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {emailData.slice(0, 3).map((row) => (
            <Table.Row key={row.id}>
              <Table.Cell>{row.subject}</Table.Cell>
              <Table.Cell>{row.from}</Table.Cell>
              <Table.Cell>{row.date}</Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

选中行

Table.Row 上使用 variant="selected" 可高亮选中行。

SubjectFromDate
Kumo v1.0.0 releasedVisal In5 seconds ago
New Job OfferCloudflare10 minutes ago
Daily Email DigestCloudflare1 hour ago
import { useState } from "react";
import { LayerCard, Table } from "@cloudflare/kumo";

export function TableSelectedRowDemo() {
  const rows = emailData.slice(0, 3);
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(["2"]));

  const toggleRow = (id: string) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  };

  const toggleAll = () => {
    if (selectedIds.size === rows.length) {
      setSelectedIds(new Set());
    } else {
      setSelectedIds(new Set(rows.map((r) => r.id)));
    }
  };

  return (
    <LayerCard className="p-0">
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.CheckHead
              checked={selectedIds.size === rows.length}
              indeterminate={
                selectedIds.size > 0 && selectedIds.size < rows.length
              }
              onCheckedChange={toggleAll}
              aria-label="Select all rows"
            />
            <Table.Head>Subject</Table.Head>
            <Table.Head>From</Table.Head>
            <Table.Head>Date</Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {rows.map((row) => (
            <Table.Row
              key={row.id}
              variant={selectedIds.has(row.id) ? "selected" : "default"}
            >
              <Table.CheckCell
                checked={selectedIds.has(row.id)}
                onCheckedChange={() => toggleRow(row.id)}
                aria-label={`Select ${row.subject}`}
              />
              <Table.Cell>{row.subject}</Table.Cell>
              <Table.Cell>{row.from}</Table.Cell>
              <Table.Cell>{row.date}</Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

固定布局与列宽

如需精确控制列宽,可设置 layout="fixed" 并使用带 col 元素的 colgroup

SubjectFromDate
Kumo v1.0.0 releasedVisal In5 seconds ago
New Job OfferCloudflare10 minutes ago
Daily Email DigestCloudflare1 hour ago
GitLab - New CommentRob Knecht1 day ago
Out of OfficeJohnnie Lappen3 days ago
import { LayerCard, Table } from "@cloudflare/kumo";

export function TableFixedLayoutDemo() {
  return (
    <LayerCard className="p-0">
      <Table layout="fixed">
        <colgroup>
          <col />
          <col className="w-[150px]" />
          <col className="w-[150px]" />
        </colgroup>
        <Table.Header>
          <Table.Row>
            <Table.Head>Subject</Table.Head>
            <Table.Head>From</Table.Head>
            <Table.Head>Date</Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {emailData.map((row) => (
            <Table.Row key={row.id}>
              <Table.Cell>{row.subject}</Table.Cell>
              <Table.Cell>{row.from}</Table.Cell>
              <Table.Cell>{row.date}</Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

固定列

Table.HeadTable.Cell 上使用 sticky="left"sticky="right", 可将列固定到滚动容器的左缘或右缘。组件会自动添加不透明背景与渐变淡出效果。请将表格 包裹在 overflow-x-auto 容器中。

SubjectFromDateTagsActions
Kumo v1.0.0 releasedVisal In5 seconds ago
New Job OfferCloudflare10 minutes ago
Daily Email DigestCloudflare1 hour ago
promotion
GitLab - New CommentRob Knecht1 day ago
Out of OfficeJohnnie Lappen3 days ago
import { Badge, Button, DropdownMenu, LayerCard, Table } from "@cloudflare/kumo";
import { DotsThree, Eye, PencilSimple, Trash } from "@phosphor-icons/react";

/**
 * Demonstrates a sticky right-hand actions column that stays pinned while the
 * table scrolls horizontally. Uses `sticky="right"` on `Table.Head` and
 * `Table.Cell` — the gradient fade and opaque background are applied
 * automatically.
 */
export function TableStickyColumnDemo() {
  return (
    <LayerCard className="w-full max-w-md overflow-x-auto p-0">
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.Head>Subject</Table.Head>
            <Table.Head>From</Table.Head>
            <Table.Head>Date</Table.Head>
            <Table.Head>Tags</Table.Head>
            <Table.Head sticky="right">
              <span className="sr-only">Actions</span>
            </Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {emailData.map((row) => (
            <Table.Row key={row.id}>
              <Table.Cell className="whitespace-nowrap">
                {row.subject}
              </Table.Cell>
              <Table.Cell className="whitespace-nowrap">{row.from}</Table.Cell>
              <Table.Cell className="whitespace-nowrap">{row.date}</Table.Cell>
              <Table.Cell className="whitespace-nowrap">
                {row.tags ? (
                  <div className="inline-flex gap-1">
                    {row.tags.map((tag) => (
                      <Badge key={tag}>{tag}</Badge>
                    ))}
                  </div>
                ) : (
                  "—"
                )}
              </Table.Cell>
              <Table.Cell sticky="right" className="text-right">
                <DropdownMenu>
                  <DropdownMenu.Trigger
                    render={
                      <Button
                        variant="ghost"
                        size="sm"
                        shape="square"
                        aria-label="More options"
                      >
                        <DotsThree weight="bold" size={16} />
                      </Button>
                    }
                  />
                  <DropdownMenu.Content>
                    <DropdownMenu.Item icon={Eye}>View</DropdownMenu.Item>
                    <DropdownMenu.Item icon={PencilSimple}>
                      Edit
                    </DropdownMenu.Item>
                    <DropdownMenu.Separator />
                    <DropdownMenu.Item icon={Trash} variant="danger">
                      Delete
                    </DropdownMenu.Item>
                  </DropdownMenu.Content>
                </DropdownMenu>
              </Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

紧凑表头与固定列

Table.Header 上组合使用 variant="compact"sticky 固定列。

SubjectFromDateTagsActions
Kumo v1.0.0 releasedVisal In5 seconds ago
New Job OfferCloudflare10 minutes ago
Daily Email DigestCloudflare1 hour ago
promotion
GitLab - New CommentRob Knecht1 day ago
Out of OfficeJohnnie Lappen3 days ago
import { Badge, Button, DropdownMenu, LayerCard, Table } from "@cloudflare/kumo";
import { DotsThree, Eye, PencilSimple, Trash } from "@phosphor-icons/react";

/**
 * Demonstrates a compact header combined with sticky columns. This combination
 * may exhibit visual inconsistencies between the compact header background
 * (`bg-kumo-elevated`) and the sticky cell background (`bg-kumo-base`).
 */
export function TableCompactStickyDemo() {
  return (
    <LayerCard className="w-full max-w-md overflow-x-auto p-0">
      <Table>
        <Table.Header variant="compact">
          <Table.Row>
            <Table.Head>Subject</Table.Head>
            <Table.Head>From</Table.Head>
            <Table.Head>Date</Table.Head>
            <Table.Head>Tags</Table.Head>
            <Table.Head sticky="right">
              <span className="sr-only">Actions</span>
            </Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {emailData.map((row) => (
            <Table.Row key={row.id}>
              <Table.Cell className="whitespace-nowrap">
                {row.subject}
              </Table.Cell>
              <Table.Cell className="whitespace-nowrap">{row.from}</Table.Cell>
              <Table.Cell className="whitespace-nowrap">{row.date}</Table.Cell>
              <Table.Cell className="whitespace-nowrap">
                {row.tags ? (
                  <div className="inline-flex gap-1">
                    {row.tags.map((tag) => (
                      <Badge key={tag}>{tag}</Badge>
                    ))}
                  </div>
                ) : (
                  "—"
                )}
              </Table.Cell>
              <Table.Cell sticky="right" className="text-right">
                <DropdownMenu>
                  <DropdownMenu.Trigger
                    render={
                      <Button
                        variant="ghost"
                        size="sm"
                        shape="square"
                        aria-label="More options"
                      >
                        <DotsThree weight="bold" size={16} />
                      </Button>
                    }
                  />
                  <DropdownMenu.Content>
                    <DropdownMenu.Item icon={Eye}>View</DropdownMenu.Item>
                    <DropdownMenu.Item icon={PencilSimple}>
                      Edit
                    </DropdownMenu.Item>
                    <DropdownMenu.Separator />
                    <DropdownMenu.Item icon={Trash} variant="danger">
                      Delete
                    </DropdownMenu.Item>
                  </DropdownMenu.Content>
                </DropdownMenu>
              </Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

完整示例

包含复选框、徽章、操作按钮与固定列宽的完整表格。

SubjectFromDate
Kumo v1.0.0 released
Visal In5 seconds ago
New Job Offer
Cloudflare10 minutes ago
Daily Email Digest
promotion
Cloudflare1 hour ago
GitLab - New Comment
Rob Knecht1 day ago
Out of Office
Johnnie Lappen3 days ago
import { useState } from "react";
import { Badge, Button, DropdownMenu, LayerCard, Table } from "@cloudflare/kumo";
import { DotsThree, EnvelopeSimple, Eye, PencilSimple, Trash } from "@phosphor-icons/react";

export function TableFullDemo() {
  const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set(["2"]));

  const toggleRow = (id: string) => {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  };

  const toggleAll = () => {
    if (selectedIds.size === emailData.length) {
      setSelectedIds(new Set());
    } else {
      setSelectedIds(new Set(emailData.map((r) => r.id)));
    }
  };

  return (
    <LayerCard className="w-full overflow-x-auto p-0">
      <Table layout="fixed">
        <colgroup>
          <col />{" "}
          {/* Checkbox column - width handled by Table.CheckHead/CheckCell */}
          <col />
          <col style={{ width: "150px" }} />
          <col style={{ width: "120px" }} />
          <col style={{ width: "50px" }} />
        </colgroup>
        <Table.Header>
          <Table.Row>
            <Table.CheckHead
              checked={selectedIds.size === emailData.length}
              indeterminate={
                selectedIds.size > 0 && selectedIds.size < emailData.length
              }
              onCheckedChange={toggleAll}
              aria-label="Select all rows"
            />
            <Table.Head>Subject</Table.Head>
            <Table.Head>From</Table.Head>
            <Table.Head>Date</Table.Head>
            <Table.Head></Table.Head>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {emailData.map((row) => (
            <Table.Row
              key={row.id}
              variant={selectedIds.has(row.id) ? "selected" : "default"}
            >
              <Table.CheckCell
                checked={selectedIds.has(row.id)}
                onCheckedChange={() => toggleRow(row.id)}
                aria-label={`Select ${row.subject}`}
              />
              <Table.Cell>
                <div className="flex items-center gap-2">
                  <EnvelopeSimple size={16} />
                  <span className="truncate">{row.subject}</span>
                  {row.tags && (
                    <div className="ml-2 inline-flex gap-1">
                      {row.tags.map((tag) => (
                        <Badge key={tag}>{tag}</Badge>
                      ))}
                    </div>
                  )}
                </div>
              </Table.Cell>
              <Table.Cell>
                <span className="truncate">{row.from}</span>
              </Table.Cell>
              <Table.Cell>
                <span className="truncate">{row.date}</span>
              </Table.Cell>
              <Table.Cell className="text-right">
                <DropdownMenu>
                  <DropdownMenu.Trigger
                    render={
                      <Button
                        variant="ghost"
                        size="sm"
                        shape="square"
                        aria-label="More options"
                      >
                        <DotsThree weight="bold" size={16} />
                      </Button>
                    }
                  />
                  <DropdownMenu.Content>
                    <DropdownMenu.Item icon={Eye}>View</DropdownMenu.Item>
                    <DropdownMenu.Item icon={PencilSimple}>
                      Edit
                    </DropdownMenu.Item>
                    <DropdownMenu.Separator />
                    <DropdownMenu.Item icon={Trash} variant="danger">
                      Delete
                    </DropdownMenu.Item>
                  </DropdownMenu.Content>
                </DropdownMenu>
              </Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table>
    </LayerCard>
  );
}

API 参考

Table

根表格组件,渲染语义化的 <table> 元素。

PropTypeDefaultDescription
layout"auto" | "fixed""auto"-
variant"default" | "selected""default"-
sticky"left" | "right"--
classNamestring-Additional CSS classes
childrenReactNode-Child elements

Table.Header

表头区域,渲染 <thead>。设置 sticky 可将表头行固定在滚动容器顶部。

Table.Body

表体区域,渲染 <tbody>

Table.Row

表格行,支持 variant="selected" 用于高亮。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Table.Head

表头单元格,渲染 <th>。支持 sticky="left"sticky="right" 固定所在列。

Table.Cell

表体单元格,渲染 <td>。支持 sticky="left"sticky="right" 固定所在列。

Table.CheckHead

带复选框的表头单元格,用于全选。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Table.CheckCell

带复选框的表体单元格,用于行选择。

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Table.ResizeHandle

用于调整列宽的可拖拽手柄。可搭配 TanStack Table 或自定义缩放逻辑使用。

TanStack Table 集成

如需排序、筛选、可调整列宽等高级功能,可与 TanStack Table 集成。Table 组件专为与 TanStack 的无头 API 无缝协作而设计。

import {
  flexRender,
  getCoreRowModel,
  useReactTable,
} from "@tanstack/react-table";
import { Table } from "@cloudflare/kumo";

function DataTable({ data, columns }) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    columnResizeMode: "onChange",
  });

  return (
    <Table layout="fixed">
      <colgroup>
        {table.getAllColumns().map((column) => (
          <col key={column.id} style={{ width: column.getSize() }} />
        ))}
      </colgroup>
      <Table.Header>
        {table.getHeaderGroups().map((headerGroup) => (
          <Table.Row key={headerGroup.id}>
            {headerGroup.headers.map((header) => (
              <Table.Head key={header.id}>
                {flexRender(
                  header.column.columnDef.header,
                  header.getContext(),
                )}
                <Table.ResizeHandle
                  onMouseDown={header.getResizeHandler()}
                  onTouchStart={header.getResizeHandler()}
                />
              </Table.Head>
            ))}
          </Table.Row>
        ))}
      </Table.Header>
      <Table.Body>
        {table.getRowModel().rows.map((row) => (
          <Table.Row key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <Table.Cell key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </Table.Cell>
            ))}
          </Table.Row>
        ))}
      </Table.Body>
    </Table>
  );
}

无障碍

语义化 HTML

Table 使用语义化的 <table><thead><tbody><th><td> 元素,以便屏幕阅读器正确导航。

复选框标签

请始终为 Table.CheckHeadTable.CheckCell 提供 aria-label,说明其用途。

键盘导航

Tab 可在各交互元素之间移动焦点。复选框响应 Space