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>
      <LayerCard.Primary 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.Primary>
    </LayerCard>
  );
}

Installation

Barrel

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

Granular

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

Usage

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

export default function Example() {
  return (
    <LayerCard>
      <LayerCard.Primary 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.Primary>
    </LayerCard>
  );
}

Examples

With Checkboxes

Add row selection with Table.CheckHead and Table.CheckCell.

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>
      <LayerCard.Primary className="p-0">
        <Table>
          <Table.Header>
            <Table.Row>
              <Table.CheckHead
                checked={selectedIds.size === rows.length}
                indeterminate={
                  selectedIds.size > 0 && selectedIds.size < rows.length
                }
                onValueChange={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)}
                  onValueChange={() => 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.Primary>
    </LayerCard>
  );
}

Compact Header

Use variant="compact" on Table.Header for a more condensed header style.

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>
      <LayerCard.Primary 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.Primary>
    </LayerCard>
  );
}

Selected Row

Use variant="selected" on Table.Row to highlight selected rows.

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>
      <LayerCard.Primary className="p-0">
        <Table>
          <Table.Header>
            <Table.Row>
              <Table.CheckHead
                checked={selectedIds.size === rows.length}
                indeterminate={
                  selectedIds.size > 0 && selectedIds.size < rows.length
                }
                onValueChange={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)}
                  onValueChange={() => 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.Primary>
    </LayerCard>
  );
}

Fixed Layout with Column Sizes

For precise control over column widths, set layout="fixed" and use colgroup with col elements.

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>
      <LayerCard.Primary 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.Primary>
    </LayerCard>
  );
}

Full Example

Complete table with checkboxes, badges, action buttons, and fixed column widths.

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>
      <LayerCard.Primary 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
                }
                onValueChange={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)}
                  onValueChange={() => 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.Primary>
    </LayerCard>
  );
}

API Reference

Table

Root table component. Renders a semantic <table> element.

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

Table.Header

Table header section. Renders <thead>.

Table.Body

Table body section. Renders <tbody>.

Table.Row

Table row. Supports variant="selected" for highlighting.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Table.Head

Header cell. Renders <th>.

Table.Cell

Body cell. Renders <td>.

Table.CheckHead

Header cell with checkbox for “select all” functionality.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Table.CheckCell

Body cell with checkbox for row selection.

PropTypeDefault

No component-specific props. Accepts standard HTML attributes.

Table.ResizeHandle

Draggable handle for column resizing. Use with TanStack Table or custom resize logic.

TanStack Table Integration

For advanced features like sorting, filtering, and resizable columns, integrate with TanStack Table. The Table component is designed to work seamlessly with TanStack’s headless 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>
  );
}

Accessibility

Semantic HTML

Table uses semantic <table>, <thead>, <tbody>, <th>, and <td> elements for proper screen reader navigation.

Checkbox Labels

Always provide aria-label for Table.CheckHead and Table.CheckCell to describe their purpose.

Keyboard Navigation

Tab moves focus through interactive elements. Checkboxes respond to Space.