Skip to content
Lenso UI

Table

Tables display structured data in rows and columns with support for sorting, selection, column resizing, and infinite scrolling.

Usage

import { Table } from '@lenso/ui';
"use client";
// Adapted from HeroUI v3.2.6 table-basic (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Table } from "@lenso/ui";
const rows = [  { key: "kate", name: "Kate Moore", role: "CEO", status: "Active", email: "[email protected]" },  { key: "john", name: "John Smith", role: "CTO", status: "Active", email: "[email protected]" },  { key: "sara", name: "Sara Johnson", role: "CMO", status: "On Leave", email: "[email protected]" },  {    key: "michael",    name: "Michael Brown",    role: "CFO",    status: "Active",    email: "[email protected]",  },] as const;const styles = stylex.create({ content: { minWidth: 600 } });
export function Basic() {  return (    <Table>      <Table.ScrollContainer>        <Table.Content aria-label="Team members" xstyle={styles.content}>          <Table.Header>            <Table.Column columnKey="name">Name</Table.Column>            <Table.Column columnKey="role">Role</Table.Column>            <Table.Column columnKey="status">Status</Table.Column>            <Table.Column columnKey="email">Email</Table.Column>          </Table.Header>          <Table.Body>            <Table.Collection items={rows}>              {(row) => (                <Table.Row itemKey={row.key}>                  <Table.Cell>{row.name}</Table.Cell>                  <Table.Cell>{row.role}</Table.Cell>                  <Table.Cell>{row.status}</Table.Cell>                  <Table.Cell>{row.email}</Table.Cell>                </Table.Row>              )}            </Table.Collection>          </Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anatomy

import { Table } from '@lenso/ui';
export default () => (  <Table>    <Table.ScrollContainer>      <Table.Content aria-label="Example table">        <Table.Header>          <Table.Column>Name</Table.Column>          <Table.Column>Role</Table.Column>        </Table.Header>        <Table.Body>          <Table.Row>            <Table.Cell>Kate Moore</Table.Cell>            <Table.Cell>CEO</Table.Cell>          </Table.Row>        </Table.Body>      </Table.Content>    </Table.ScrollContainer>    <Table.Footer>{/* Optional footer content */}</Table.Footer>  </Table>);

Examples

Secondary Variant

"use client";// HeroUI v3.2.6 secondary-variant adaptation (Apache-2.0).import { Table } from "@lenso/ui";import { Headers, Rows, styles, users } from "./data";export function SecondaryVariant() {  return (    <Table variant="secondary">      <Table.ScrollContainer>        <Table.Content aria-label="Team members" xstyle={styles.content}>          <Headers />          <Table.Body>            <Rows items={users.slice(0, 4)} />          </Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Async Loading

Use Table.LoadMore for infinite scrolling. It renders a sentinel row that triggers onLoadMore when scrolled into view.

"use client";// HeroUI v3.2.6 async-loading adaptation (Apache-2.0).import { useCallback, useEffect, useRef, useState } from "react";import { Spinner, Table } from "@lenso/ui";import { Rows, styles, users } from "./data";export function AsyncLoading() {  const [count, setCount] = useState(6);  const [loading, setLoading] = useState(false);  const request = useRef<ReturnType<typeof setTimeout> | null>(null);  useEffect(    () => () => {      if (request.current) clearTimeout(request.current);    },    [],  );  const loadMore = useCallback(() => {    if (request.current || count >= users.length) return;    setLoading(true);    request.current = setTimeout(() => {      request.current = null;      setCount((previous) => previous + 6);      setLoading(false);    }, 1500);  }, [count]);  return (    <Table>      <Table.ScrollContainer xstyle={styles.viewport}>        <Table.Content aria-label="Async loading table" xstyle={styles.content}>          <Table.Header xstyle={styles.sticky}>            {["name", "role", "status", "email"].map((column) => (              <Table.Column key={column} columnKey={column}>                {column[0]!.toUpperCase() + column.slice(1)}              </Table.Column>            ))}          </Table.Header>          <Table.Body>            <Rows items={users.slice(0, count)} chips />            {count < users.length && (              <Table.LoadMore colSpan={4} loading={loading} hasMore onLoadMore={loadMore}>                <Table.LoadMoreContent>                  <Spinner size="md" />                </Table.LoadMoreContent>              </Table.LoadMore>            )}          </Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Sorting

Columns can be made sortable using the allowsSorting prop on Table.Column. Use sortDescriptor and onSortChange on Table.Content to manage sort state. Wrap the label in Table.SortableColumnHeader and forward the sortDirection from the column's render prop to render the default ascending/descending indicator.

"use client";// HeroUI v3.2.6 sorting adaptation (Apache-2.0).import { useState } from "react";import { Table, type SortDescriptor } from "@lenso/ui";import { Headers, Rows, styles, users, type User } from "./data";export function Sorting() {  const [sort, setSort] = useState<SortDescriptor>({ column: "name", direction: "ascending" });  const sorted = users    .slice(0, 5)    .sort(      (a, b) =>        String(a[sort.column as keyof User]).localeCompare(String(b[sort.column as keyof User])) *        (sort.direction === "ascending" ? 1 : -1),    );  return (    <Table>      <Table.ScrollContainer>        <Table.Content          aria-label="Sortable table"          xstyle={styles.content}          sortDescriptor={sort}          onSortChange={setSort}        >          <Headers sorting />          <Table.Body>            <Rows items={sorted} />          </Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Selection

Enable row selection with selectionMode on Table.Content. Use Checkbox with slot="selection" for select-all and per-row checkboxes.

"use client";// HeroUI v3.2.6 selection adaptation (Apache-2.0).import { useState, type Key } from "react";import * as stylex from "@stylexjs/stylex";import { Table } from "@lenso/ui";import { Headers, Rows, styles, users } from "./data";export function SelectionDemo() {  const [selected, setSelected] = useState<Set<Key>>(new Set());  return (    <div {...stylex.props(styles.stack)}>      <Table>        <Table.ScrollContainer>          <Table.Content            aria-label="Table with selection"            xstyle={styles.content}            selectionMode="multiple"            selectedKeys={selected}            onSelectionChange={setSelected}          >            <Headers selection />            <Table.Body>              <Rows items={users.slice(0, 4)} selection />            </Table.Body>          </Table.Content>        </Table.ScrollContainer>      </Table>      <p {...stylex.props(styles.muted)}>        Selected: {selected.size ? [...selected].join(", ") : "None"}      </p>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Expandable Rows

Rows can be nested to display hierarchical data. Use the treeColumn prop to designate a column, and render a Button with slot="chevron" in that column’s cells so users can expand and collapse the row. Use the expandedKeys prop to control which rows are expanded.

"use client";// HeroUI v3.2.6 expandable-rows adaptation (Apache-2.0).import { useState, type Key } from "react";import * as stylex from "@stylexjs/stylex";import { Icon } from "@iconify/react";import { Table } from "@lenso/ui";type Row = { key: string; title: string; type: string; date: string; children: Row[] };const file = (key: string, title: string, date: string, children: Row[] = []): Row => ({  key,  title,  date,  children,  type: children.length ? "Directory" : "File",});const rows = [  file("1", "Documents", "10/20/2025", [    file("2", "Project", "8/2/2025", [      file("3", "Weekly Report", "7/10/2025"),      file("4", "Budget", "8/20/2025"),    ]),  ]),  file("5", "Photos", "2/3/2026", [    file("6", "Image 1", "1/23/2026"),    file("7", "Image 2", "2/3/2026"),  ]),];const styles = stylex.create({  content: { minWidth: 520 },  title: { display: "flex", alignItems: "center", gap: 4 },  chevron: { transform: "rotate(90deg)" },  collapsed: { transform: { default: "none", ":dir(rtl)": "rotate(180deg)" } },});export function ExpandableRows() {  const [expanded, setExpanded] = useState<Set<Key>>(new Set(["1"]));  const renderRow = (item: Row): React.ReactNode => (    <Table.Row itemKey={item.key} textValue={item.title}>      <Table.Cell columnKey="name">        {({ hasChildItems, isExpanded, isTreeColumn }) => (          <span {...stylex.props(styles.title)}>            {hasChildItems && isTreeColumn && (              <Table.ExpandButton aria-label={`Toggle ${item.title}`}>                <Icon                  icon="gravity-ui:chevron-right"                  width={16}                  aria-hidden="true"                  {...stylex.props(isExpanded ? styles.chevron : styles.collapsed)}                />              </Table.ExpandButton>            )}            <span>{item.title}</span>          </span>        )}      </Table.Cell>      <Table.Cell>{item.type}</Table.Cell>      <Table.Cell>{item.date}</Table.Cell>      <Table.Collection items={item.children}>{renderRow}</Table.Collection>    </Table.Row>  );  return (    <Table>      <Table.ScrollContainer>        <Table.Content          aria-label="Files"          xstyle={styles.content}          treeColumn="name"          expandedKeys={expanded}          onExpandedChange={setExpanded}        >          <Table.Header>            <Table.Column columnKey="name">Name</Table.Column>            <Table.Column columnKey="type">Type</Table.Column>            <Table.Column columnKey="date">Date Modified</Table.Column>          </Table.Header>          <Table.Body items={rows}>{renderRow}</Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Pagination

Use Table.Footer to add a pagination component below the table.

"use client";// HeroUI v3.2.6 pagination adaptation (Apache-2.0).import { useState } from "react";import { Table } from "@lenso/ui";import { Headers, Pages, Rows, styles, users } from "./data";export function PaginationDemo() {  const [page, setPage] = useState(1);  return (    <Table>      <Table.ScrollContainer>        <Table.Content aria-label="Table with pagination" xstyle={styles.content}>          <Headers />          <Table.Body>            <Rows items={users.slice((page - 1) * 4, page * 4)} />          </Table.Body>        </Table.Content>      </Table.ScrollContainer>      <Table.Footer>        <Pages page={page} count={2} total={8} setPage={setPage} />      </Table.Footer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Column Resizing

Wrap the table in Table.ResizableContainer and add Table.ColumnResizer inside each resizable column.

"use client";// HeroUI v3.2.6 column-resizing adaptation (Apache-2.0).import { Table } from "@lenso/ui";import { Headers, Rows, styles, users } from "./data";export function ColumnResizing() {  return (    <Table>      <Table.ResizableContainer>        <Table.Content aria-label="Table with resizable columns" xstyle={styles.resizing}>          <Headers resizing />          <Table.Body>            <Rows items={users.slice(0, 5)} chips />          </Table.Body>        </Table.Content>      </Table.ResizableContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Empty State

Use renderEmptyState on Table.Body to display a custom message when the table has no data.

"use client";// HeroUI v3.2.6 empty-state adaptation (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Icon } from "@iconify/react";import { EmptyState, Table } from "@lenso/ui";import { Headers, styles } from "./data";const empty = stylex.create({  root: { minHeight: 200 },  content: {    minHeight: 140,    display: "flex",    flexDirection: "column",    alignItems: "center",    justifyContent: "center",    gap: 16,    textAlign: "center",    color: "var(--muted)",  },});export function EmptyStateDemo() {  return (    <Table xstyle={empty.root}>      <Table.ScrollContainer>        <Table.Content aria-label="Empty table" xstyle={styles.content}>          <Headers />          <Table.Body>            <tr>              <td colSpan={4}>                <EmptyState xstyle={empty.content}>                  <Icon icon="gravity-ui:tray" width={24} aria-hidden="true" />                  <span>No results found</span>                </EmptyState>              </td>            </tr>          </Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Virtualization

Table supports virtualization through Virtualizer, enabling efficient rendering of large datasets by displaying only the rows visible within the viewport.

"use client";// HeroUI v3.2.6 virtualization adaptation (Apache-2.0); native row spacers.import { Table } from "@lenso/ui";import { styles } from "./data";const first = [  "Emma",  "Liam",  "Olivia",  "Noah",  "Ava",  "James",  "Sophia",  "Oliver",  "Isabella",  "Lucas",  "Mia",  "Ethan",  "Charlotte",  "Mason",  "Amelia",  "Logan",  "Harper",  "Alexander",  "Ella",  "Benjamin",];const last = [  "Smith",  "Johnson",  "Williams",  "Brown",  "Jones",  "Garcia",  "Miller",  "Davis",  "Rodriguez",  "Martinez",  "Anderson",  "Taylor",  "Thomas",  "Jackson",  "White",  "Harris",  "Clark",  "Lewis",  "Robinson",  "Walker",];const roles = [  "Software Engineer",  "Senior Engineer",  "Staff Engineer",  "Product Manager",  "Designer",  "Data Analyst",  "QA Engineer",  "DevOps Engineer",  "Marketing Manager",  "Sales Representative",];const users = Array.from({ length: 1000 }, (_, index) => ({  key: index + 1,  name: `${first[index % 20]} ${last[Math.floor(index / 20) % 20]}`,  role: roles[index % 10],  email: `${first[index % 20]!.toLowerCase()}.${last[Math.floor(index / 20) % 20]!.toLowerCase()}@acme.com`,}));export function Virtualization() {  return (    <Table>      <Table.ScrollContainer xstyle={styles.virtualViewport}>        <Table.Content aria-label="Virtualized table with 1000 rows" xstyle={styles.resizing}>          <Table.Header xstyle={styles.sticky}>            <Table.Column columnKey="name" minWidth={160}>              Name            </Table.Column>            <Table.Column columnKey="role" minWidth={220}>              Role            </Table.Column>            <Table.Column columnKey="email" minWidth={240}>              Email            </Table.Column>          </Table.Header>          <Table.Body items={users} virtualized={{ height: 300, rowHeight: 42 }}>            {(user) => (              <Table.Row itemKey={user.key}>                <Table.Cell>{user.name}</Table.Cell>                <Table.Cell>{user.role}</Table.Cell>                <Table.Cell>{user.email}</Table.Cell>              </Table.Row>            )}          </Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

TanStack Table

HeroUI's Table works as a rendering layer on top of headless table libraries. This example uses TanStack Table for column definitions, sorting, and pagination — while HeroUI handles styling and accessibility.

"use client";// HeroUI v3.2.6 tanstack-table adaptation (Apache-2.0); retains the v9 data model.import { useState } from "react";import { Table, type SortDescriptor } from "@lenso/ui";import {  createColumnHelper,  createPaginatedRowModel,  createSortedRowModel,  flexRender,  rowPaginationFeature,  rowSortingFeature,  sortFn_alphanumeric,  tableFeatures,  useTable,  type SortingState,} from "@tanstack/react-table";import { Pages, Status, styles, users, type User } from "./data";const features = tableFeatures({  paginatedRowModel: createPaginatedRowModel(),  rowPaginationFeature,  rowSortingFeature,  sortFns: { alphanumeric: sortFn_alphanumeric },  sortedRowModel: createSortedRowModel(),});const helper = createColumnHelper<typeof features, User>();const columns = helper.columns([  helper.accessor("name", { header: "Name" }),  helper.accessor("role", { header: "Role" }),  helper.accessor("status", {    header: "Status",    cell: (info) => <Status status={info.getValue()} />,  }),  helper.accessor("email", { header: "Email" }),]);const data = users.slice(0, 8);export function TanstackTable() {  const [sorting, setSorting] = useState<SortingState>([]);  const table = useTable({    columns,    data,    features,    initialState: { pagination: { pageIndex: 0, pageSize: 4 } },    onSortingChange: setSorting,    state: { sorting },  });  const first = sorting[0];  const descriptor: SortDescriptor | undefined = first    ? { column: first.id, direction: first.desc ? "descending" : "ascending" }    : undefined;  return (    <Table>      <Table.ScrollContainer>        <Table.Content          aria-label="TanStack Table example"          xstyle={styles.content}          sortDescriptor={descriptor}          onSortChange={(next) =>            setSorting([{ id: String(next.column), desc: next.direction === "descending" }])          }        >          <Table.Header>            {table.getHeaderGroups()[0]?.headers.map((header) => (              <Table.Column                key={header.id}                columnKey={header.id}                allowsSorting={header.column.getCanSort()}              >                {({ sortDirection }) => (                  <Table.SortableColumnHeader sortDirection={sortDirection}>                    {flexRender(header.column.columnDef.header, header.getContext())}                  </Table.SortableColumnHeader>                )}              </Table.Column>            ))}          </Table.Header>          <Table.Body>            {table.getRowModel().rows.map((row) => (              <Table.Row key={row.id} itemKey={row.id}>                {row.getAllCells().map((cell) => (                  <Table.Cell key={cell.id}>                    {flexRender(cell.column.columnDef.cell, cell.getContext())}                  </Table.Cell>                ))}              </Table.Row>            ))}          </Table.Body>        </Table.Content>      </Table.ScrollContainer>      <Table.Footer>        <Pages          page={table.state.pagination.pageIndex + 1}          count={table.getPageCount()}          total={8}          setPage={(page) => table.setPageIndex(page - 1)}        />      </Table.Footer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Custom Cells

"use client";// HeroUI v3.2.6 custom-cells adaptation (Apache-2.0).import { useState, type Key } from "react";import * as stylex from "@stylexjs/stylex";import { Icon } from "@iconify/react";import { Avatar, Button, Table, type SortDescriptor } from "@lenso/ui";import { Status, styles, users, type User } from "./data";const workers = users.slice(0, 5).map((user, index) => ({  ...user,  key: [4586932, 5273849, 7492836, 8293746, 1234567][index]!,  role: [    "Chief Executive Officer",    "Chief Technology Officer",    "Chief Marketing Officer",    "Chief Financial Officer",    "Product Manager",  ][index]!,  color: ["red", "green", "blue", "purple", "orange"][index],}));export function CustomCells() {  const [selected, setSelected] = useState<Set<Key>>(new Set());  const [sort, setSort] = useState<SortDescriptor>({ column: "name", direction: "ascending" });  const sorted = [...workers].sort(    (a, b) =>      String(a[sort.column as keyof User]).localeCompare(String(b[sort.column as keyof User])) *      (sort.direction === "ascending" ? 1 : -1),  );  return (    <Table>      <Table.ScrollContainer>        <Table.Content          aria-label="Table with custom cells"          xstyle={styles.customCells}          selectionMode="multiple"          selectedKeys={selected}          onSelectionChange={setSelected}          sortDescriptor={sort}          onSortChange={setSort}        >          <Table.Header>            <Table.Column columnKey="selection" width={48}>              <Table.SelectionCheckbox />            </Table.Column>            {[              ["key", "Worker ID"],              ["name", "Member"],              ["role", "Role"],              ["status", "Status"],            ].map(([key, title]) => (              <Table.Column key={key} columnKey={key!} allowsSorting>                {({ sortDirection }) => (                  <Table.SortableColumnHeader sortDirection={sortDirection}>                    {title}                  </Table.SortableColumnHeader>                )}              </Table.Column>            ))}            <Table.Column columnKey="actions">Actions</Table.Column>          </Table.Header>          <Table.Body>            {sorted.map((user) => (              <Table.Row key={user.key} itemKey={user.key}>                <Table.Cell>                  <Table.SelectionCheckbox aria-label={`Select ${user.name}`} />                </Table.Cell>                <Table.Cell>                  <div {...stylex.props(styles.row)}>                    #{user.key}                    <Button                      isIconOnly                      aria-label={`Copy ID #${user.key}`}                      size="sm"                      variant="ghost"                    >                      <Icon icon="gravity-ui:copy" width={16} aria-hidden="true" />                    </Button>                  </div>                </Table.Cell>                <Table.Cell>                  <div {...stylex.props(styles.row)}>                    <Avatar size="sm">                      <Avatar.Image                        alt={user.name}                        src={`https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/${user.color}.jpg`}                      />                      <Avatar.Fallback>                        {user.name                          .split(" ")                          .map((name) => name[0])                          .join("")}                      </Avatar.Fallback>                    </Avatar>                    <div {...stylex.props(styles.detail)}>                      <span>{user.name}</span>                      <span {...stylex.props(styles.muted)}>{user.email}</span>                    </div>                  </div>                </Table.Cell>                <Table.Cell>{user.role}</Table.Cell>                <Table.Cell>                  <Status status={user.status} />                </Table.Cell>                <Table.Cell>                  <div {...stylex.props(styles.row)}>                    {(["View", "Edit", "Delete"] as const).map((action, index) => (                      <Button                        key={action}                        isIconOnly                        aria-label={`${action} ${user.name}`}                        size="sm"                        variant={action === "Delete" ? "danger-soft" : "tertiary"}                      >                        <Icon                          icon={`gravity-ui:${["eye", "pencil", "trash-bin"][index]}`}                          width={16}                          aria-hidden="true"                        />                      </Button>                    ))}                  </div>                </Table.Cell>              </Table.Row>            ))}          </Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Customization

Tailwind CSS

"use client";// HeroUI v3.2.6 custom-styles adaptation (Apache-2.0).import { Table } from "@lenso/ui";import { Headers, Rows, styles, users } from "./data";export function CustomStyles() {  return (    <Table xstyle={styles.customRoot}>      <Table.ScrollContainer>        <Table.Content aria-label="Team members" xstyle={styles.content}>          <Headers custom email={false} />          <Table.Body>            <Rows items={users.slice(0, 3)} custom email={false} />          </Table.Body>        </Table.Content>      </Table.ScrollContainer>    </Table>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

To customize the Table component classes, you can use the @layer components directive. Learn more.

@layer components {  .table-root {    @apply relative grid w-full overflow-clip;  }
  .table__header {    @apply bg-gray-100;  }
  .table__column {    @apply px-4 py-2.5 text-left text-xs font-medium text-gray-600;  }
  .table__row {    @apply bg-white border-b border-gray-200;  }
  .table__cell {    @apply px-4 py-3 text-sm;  }
  .table__footer {    @apply flex items-center px-4 py-2.5;  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

The Table component uses these CSS classes (View source styles):

Base Classes [!toc]

  • .table-root - Root container (named table-root instead of table because table is a built-in Tailwind CSS utility class for display: table)
  • .table__scroll-container - Horizontal scroll wrapper with custom scrollbar
  • .table__content - The <table> element
  • .table__header - Header row (<thead>)
  • .table__column - Column header cell (<th>)
  • .table__body - Body section (<tbody>)
  • .table__row - Row element (<tr>)
  • .table__cell - Data cell (<td>)
  • .table__footer - Footer container (outside table)

Advanced Classes [!toc]

  • .table__column-resizer - Drag handle for column resizing
  • .table__resizable-container - Wrapper enabling column resizing
  • .table__load-more - Sentinel row for infinite scrolling
  • .table__load-more-content - Styled container for the loading indicator

Variant Classes [!toc]

  • .table-root--primary - Gray background container with card-style body (default)
  • .table-root--secondary - No background, standalone rounded headers

Interactive States

The Table supports both CSS pseudo-classes and data attributes for flexibility:

  • Hover: :hover or [data-hovered="true"] (row background change)
  • Selected: [data-selected="true"] (row highlight)
  • Focus: :focus-visible or [data-focus-visible="true"] (inset focus ring on rows, columns, and cells)
  • Disabled: :disabled or [aria-disabled="true"] (reduced opacity)
  • Sortable: [data-allows-sorting="true"] (interactive cursor on columns)
  • Dragging: [data-dragging="true"] (reduced opacity)
  • Drop Target: [data-drop-target="true"] (accent background)

API Reference

Table

PropTypeDefaultDescription
variant"primary" | "secondary""primary"Visual variant. Primary has a gray background container; secondary is flat with transparent rows.
classNamestring-Additional CSS classes for the root container
childrenReact.ReactNode-Table content (ScrollContainer, Footer, etc.)

Table.ScrollContainer

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Table.Content element

Table.Content

Inherits from React Aria Table.

PropTypeDefaultDescription
aria-labelstring-Accessible label for the table
selectionMode"none" | "single" | "multiple""none"Selection behavior
selectedKeysSelection-Controlled selected keys
onSelectionChange(keys: Selection) => void-Selection change handler
sortDescriptorSortDescriptor-Current sort state
onSortChange(descriptor: SortDescriptor) => void-Sort change handler
classNamestring-Additional CSS classes

Table.Header

Inherits from React Aria TableHeader.

PropTypeDefaultDescription
columnsT[]-Dynamic column data for render prop pattern
childrenReact.ReactNode | (column: T) => React.ReactNode-Static columns or render prop

Table.Column

Inherits from React Aria Column.

PropTypeDefaultDescription
idstring-Column identifier
allowsSortingbooleanfalseWhether the column is sortable
isRowHeaderbooleanfalseWhether this column is a row header
defaultWidthstring | number-Default width for resizable columns
minWidthnumber-Minimum width for resizable columns
childrenReact.ReactNode | (values: ColumnRenderProps) => React.ReactNode-Column content or render prop with sort direction

Table.Body

Inherits from React Aria TableBody.

PropTypeDefaultDescription
itemsT[]-Dynamic row data for render prop pattern
renderEmptyState() => React.ReactNode-Content to display when the table is empty
childrenReact.ReactNode | (item: T) => React.ReactNode-Static rows or render prop

Table.Row

Inherits from React Aria Row.

PropTypeDefaultDescription
idstring | number-Row identifier
classNamestring-Additional CSS classes
childrenReact.ReactNode-Row cells

Table.Cell

Inherits from React Aria Cell.

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Cell content

Table.SortableColumnHeader

Renders a sortable column label with an ascending/descending indicator. Use it inside a Table.Column render-prop callback and forward the sortDirection value.

PropTypeDefaultDescription
sortDirection"ascending" | "descending"-Current sort direction. Pass through from the Table.Column render prop.
showIndicatorbooleantrueWhether to render the sort indicator icon when a direction is set.
indicatorReact.ReactNode-Custom indicator element. Overrides the default chevron and receives a data-direction attribute.
classNamestring-Additional CSS classes for the wrapper.
childrenReact.ReactNode-Column label content

Table.Footer

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Footer content (e.g., pagination)

Table.ColumnResizer

Inherits from React Aria ColumnResizer.

PropTypeDefaultDescription
classNamestring-Additional CSS classes

Table.ResizableContainer

Inherits from React Aria ResizableTableContainer.

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Table.Content element

Table.LoadMore

Inherits from React Aria TableLoadMoreItem.

PropTypeDefaultDescription
isLoadingbooleanfalseWhether data is currently loading
onLoadMore() => void-Handler called when the sentinel row is visible
childrenReact.ReactNode-Loading indicator content

Table.LoadMoreContent

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Loading indicator content (e.g., Spinner)

Table.Collection

Re-exported from React Aria Collection. Used to render dynamic cells within rows alongside static cells (e.g., checkboxes).

PropTypeDefaultDescription
itemsT[]-Collection items
children(item: T) => React.ReactNode-Render prop for each item

TableLayout

NameTypeDefaultDescription
rowHeightnumber | undefined48The fixed height of a row in px.
estimatedRowHeightnumber | undefined—The estimated height of a row, when row heights are variable.
headingHeightnumber | undefined48The fixed height of a section header in px.
estimatedHeadingHeightnumber | undefined—The estimated height of a section header, when the height is variable.
loaderHeightnumber | undefined48The fixed height of a loader element in px. This loader is specifically for "load more" elements rendered when loading more rows at the root level or inside nested row/sections.
dropIndicatorThicknessnumber | undefined2The thickness of the drop indicator.
gapnumber | undefined0The gap between items.
paddingnumber | undefined0The padding around the list.