# Table

- Page: https://jiguang132.com/resources/astryx/docs/table
- Resource: Astryx (https://jiguang132.com/resources/astryx.md)
- All guides: https://jiguang132.com/resources/astryx/docs.md

Table displays structured data in rows and columns with consistent dimensionality. It supports rich cell content, sorting, selection, pagination, and column management through a composable plugin system. Use Table for data sets with uniform structure; for simpler or inconsistent data, consider a list or card layout instead.

## Installation

```bash
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0
```

Requires React 19+, base CSS and a Theme provider. See [Setup](/resources/astryx/docs/getting-started).

## Table

Table displays structured data in rows and columns with consistent dimensionality. It supports rich cell content, sorting, selection, pagination, and column management through a composable plugin system. Use Table for data sets with uniform structure; for simpler or inconsistent data, consider a list or card layout instead.

| Prop | Type | Default | Description |
|---|---|---|---|
| data | T[] |  | Array of data items to render as rows. T must extend Record<string, unknown> (use `interface MyRow extends Record<string, unknown>` for custom types). |
| columns | TableColumn<T>[] |  | Column definitions: each column has {key, header, width?, align?, renderCell?}. The `header` field sets the column heading text. If omitted, columns are auto-generated from data object keys. The `width` field is typed as `ColumnWidth` (not a number); use `proportional(n)` or `pixel(n)` helpers imported from `@astryxdesign/core/Table`. Omitting `width` keeps an equal flexible share with a compact 60px readability floor. To give one column a different floor, set `width: proportional(1, {minWidth: 80})`; its `minWidth` replaces the 60px floor for that column. Example: `width: pixel(120)` for 120px fixed, `width: proportional(1)` for a flexible column with a 120px default minimum. |
| idKey | (keyof T & string) \| ((item: T) => string \| number) |  | Row key for React reconciliation. Pass a property name string or a function. Falls back to row index if omitted. |
| density | 'compact' \| 'balanced' \| 'spacious' | 'balanced' | Row density controlling cell padding and font size. |
| dividers | 'rows' \| 'columns' \| 'grid' \| 'none' | 'rows' | Divider style rendered between cells. |
| isStriped | boolean | false | Applies a background wash to even-numbered rows. |
| hasHover | boolean | false | Applies a hover highlight background to rows on pointer devices. |
| verticalAlign | 'middle' \| 'top' \| 'bottom' | 'middle' | Vertical alignment for body row cells. Controls `vertical-align` on the `<td>` elements. |
| textOverflow | 'wrap' \| 'truncate' | 'wrap' | How body cell text behaves when it exceeds the column width. 'wrap' lets text wrap and the row grow taller; 'truncate' clips with an ellipsis (default-rendered cells show a tooltip on hover when truncated). Header cells always truncate. |
| plugins | Record<string, TablePlugin<T>> |  | Named plugins that extend table behavior via the transform pipeline. Converted to an ordered array internally. |
| rowIndexStart | number | 1 | ARIA row index (1-based) for the first rendered body row. The row ordinal is an accessibility concern independent of any visible index column, so setting this (or rowCount) makes the table emit aria-rowindex on body rows and aria-rowcount on the table. For a paginated/windowed view, pass the offset of the first visible row (e.g. (page - 1) * pageSize + 1) so aria-rowindex reflects position in the full dataset. Data-driven mode only. |
| rowCount | number |  | Total number of body rows across all pages/windows, used for aria-rowcount so assistive tech can announce "row X of Y" against the full dataset. When omitted but rowIndexStart is set (windowed view with an unknown total), aria-rowcount is set to -1 per the ARIA unknown-count convention. Data-driven mode only. |
| children | ReactNode |  | Children mode: compose the table yourself from TableHeader / TableBody / TableFooter, each holding TableRow and TableCell, instead of using data-driven rendering. The children are passed straight to the <table>, so the section is yours to supply. A TableRow placed directly in Table emits <table><tr>, which is invalid HTML and mismatches on hydration (the parser inserts an implied <tbody> for server-rendered markup; React does not on the client). Data-driven mode renders the sections for you. |
| xstyle | StyleXStyles |  | StyleX styles for layout customization (margins, positioning, sizing). Must be a stylex.create() value: not an inline style object like style={{}}. |

### Usage guidance

- Use density and divider variants to match the information density and scanning needs of your data.
- Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.
- In children mode, put every row inside TableHeader, TableBody, or TableFooter. <table> cannot contain a <tr> directly: the HTML parser inserts an implied <tbody> for server-rendered markup and React does not on the client, so unwrapped rows mismatch on hydration.
- Omit width for equal flexible columns with a compact 60px readability floor. Give one column its own floor with proportional(1, {minWidth}), weight columns with proportional(n), and fix widths with pixel(); the Table scroll region activates when the combined floors no longer fit.
- Use the data-driven API from React Server Components: proportional(), pixel(), and column definitions without function props are server-safe. Columns using renderCell (or any function prop) need the table wrapped in a "use client" component, since functions cannot cross the server-client boundary.
- Avoid: Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.
- Avoid: Enable every plugin at once. Add only the features your use case requires to keep the interface focused.
- Avoid: Rely on the 60px omitted-width floor when a text-heavy column needs more room; set proportional(1, {minWidth}) on that column instead.

### Theming API

```json
{
  "targets": [
    {
      "className": "astryx-table"
    },
    {
      "className": "astryx-table-scroll-wrapper"
    },
    {
      "className": "astryx-table-header"
    },
    {
      "className": "astryx-table-body"
    },
    {
      "className": "astryx-table-footer"
    },
    {
      "className": "astryx-table-row"
    },
    {
      "className": "astryx-table-cell",
      "visualProps": [
        "density"
      ]
    },
    {
      "className": "astryx-table-header-cell",
      "visualProps": [
        "density"
      ]
    }
  ]
}
```

## TableHeader



## TableBody



## TableFooter



## TableRow



## TableCell



## TableHeaderCell



## useTableSelection



## useTableSelectionState



## TableSelectionToolbar



## useTableSortable



## useTableTreeData



## useTableTreeState



## useTablePagination



## useTableColumnSettings



## useTableFiltering



## useTableFilterState



### Anatomy

- **Table**: Semantic table element that groups the table sections, rows, and cells.
- **Scroll region**: Outer region that scrolls horizontally, enters the keyboard order, and contains overscroll only while the columns overflow.
- **Header section**: Column-heading section generated when data-driven columns are present or supplied with TableHeader in children mode.
- **Column header cell**: Cell that identifies one column and may contain sorting or bulk-selection controls.
- **Sort control**: Button that wraps a sortable column label and changes that column's sort direction.
- **Sort indicator glyph**: Directional symbol rendered by Icon inside a Sort control.
- **Sort priority**: Number shown for a sorted column when multi-column sorting is active.
- **Selection control**: CheckboxInput rendered in the header and selectable body rows by the selection plugin.
- **Body section**: Section containing data rows or the current empty state; data-driven mode renders it automatically.
- **Row**: Repeated TableRow that groups cells in a standard header, body, or footer row.
- **Cell**: TableCell containing one value or caller-provided content in a standard body or footer row.
- **Default empty state**: Compact EmptyState shown for an empty data array unless it is replaced or disabled.
- **Expansion control**: Button in a leading cell that expands or collapses one expandable row.
- **Expansion glyph**: Directional symbol rendered by Icon inside an Expansion control.
- **Expanded detail panel**: Detail row and spanning cell rendered below an expanded row around caller-provided content.
- **Footer section**: Optional summary or totals section supplied with TableFooter in children mode.

## Table Body

<tbody> wrapper for children mode. Holds the data rows. A row must sit inside a section: <table> cannot contain a <tr> directly, because the HTML parser inserts an implied <tbody> when it parses server-rendered markup and React does not when it renders on the client, so rows written straight into Table mismatch on hydration. The data-driven data={...} mode renders this element itself; in children mode it is yours to supply.

| Prop | Type | Default | Description |
|---|---|---|---|
| children * | ReactNode |  | The <tbody> rows. |
| xstyle | StyleXStyles |  | StyleX styles for layout customization. Must be a stylex.create() value: not an inline style object like style={{}}. |

### Theming API

```json
{
  "targets": [
    {
      "className": "astryx-table-body"
    }
  ]
}
```

## Table Cell

<td> wrapper that reads TableContext to apply density padding, font size, and divider borders when used inside Table.

| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode |  | Cell content. |

## Table Footer

<tfoot> wrapper for children mode. Holds summary or total rows beneath the body. A row must sit inside a section: <table> cannot contain a <tr> directly, because the HTML parser inserts an implied <tbody> when it parses server-rendered markup and React does not when it renders on the client, so the two trees mismatch on hydration. Children mode only: the data-driven data={...} path renders a header and a body, never a footer.

| Prop | Type | Default | Description |
|---|---|---|---|
| children * | ReactNode |  | The <tfoot> rows. |
| xstyle | StyleXStyles |  | StyleX styles for layout customization. Must be a stylex.create() value: not an inline style object like style={{}}. |

### Theming API

```json
{
  "targets": [
    {
      "className": "astryx-table-footer"
    }
  ]
}
```

## Table Header

<thead> wrapper for children mode. Holds the header row, whose cells are TableHeaderCell. A row must sit inside a section: <table> cannot contain a <tr> directly, because the HTML parser inserts an implied <tbody> when it parses server-rendered markup and React does not when it renders on the client, so the two trees mismatch on hydration. The data-driven data={...} mode renders this element itself whenever columns are supplied; in children mode it is yours to supply.

| Prop | Type | Default | Description |
|---|---|---|---|
| children * | ReactNode |  | The <thead> rows. |
| xstyle | StyleXStyles |  | StyleX styles for layout customization. Must be a stylex.create() value: not an inline style object like style={{}}. |

### Theming API

```json
{
  "targets": [
    {
      "className": "astryx-table-header"
    }
  ]
}
```

## Table Header Cell

<th> wrapper that reads TableContext to apply density padding, semibold weight, secondary text color, and divider borders when used inside Table.

| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode |  | Header cell content. |

## Table Row

<tr> wrapper that reads TableContext to apply striped, hover, and divider styles when used inside Table.

| Prop | Type | Default | Description |
|---|---|---|---|
| children * | ReactNode |  | Row cell elements. |

## Table Selection Toolbar

Pass both outputs of useTableSelectionState to their matching consumers: selectionConfig to useTableSelection and selectionState to TableSelectionToolbar. Render product actions through startContent. Keep fixed or floating placement in the surrounding layout.

| Prop | Type | Default | Description |
|---|---|---|---|
| selection * | TableSelectionState |  | Shared selection state returned by useTableSelectionState: selectedKeys, selectedCount, hasSelection, and clearSelection. |
| startContent | ReactNode |  | Product-owned action controls aligned to the logical start. Child controls inherit the toolbar size. |
| label | string | Bulk actions | Accessible name for the action surface. |
| renderSelectionLabel | (selectedCount: number) => ReactNode | count => `${count} selected` | Overrides the visible count text for localisation or domain-specific copy. |
| clearLabel | string | Unselect All | Visible label for the command that clears the complete controlled selection. |
| size | 'sm' \| 'md' \| 'lg' | sm | Toolbar size. Cascades to product action controls and the clear action. |
| variant | SectionVariant | muted | Toolbar surface variant. |
| xstyle | StyleXStyles |  | Caller-owned layout styles, including sticky positioning for floating compositions. |

### Usage guidance

- Pass selectionState directly from useTableSelectionState so the count and clear action stay synchronized with row selection.
- Provide product actions as startContent instead of encoding them as configuration objects.
- Avoid: Inject the toolbar through the Table plugin or its horizontal scroll wrapper.

## useTableColumnResize

Hook that returns a TablePlugin adding draggable resize handles to column header borders. Supports pointer + keyboard (WAI-ARIA window splitter) resizing, RTL, per-column min/max widths, and proportional-column preservation (resizes the neighbor so the table stays full-width). Commits widths on release.

| Prop | Type | Default | Description |
|---|---|---|---|
| columnWidths | Record<string, number> |  | Controlled pixel-width overrides keyed by column key. When a key is present it overrides the column's declared width. |
| onColumnResizeEnd | (updates: Record<string, number>) => void |  | Called when a resize completes (pointerup / Enter). Receives a map of every column key whose width changed; merge it into your columnWidths state. |
| minWidth | number |  | Global minimum column width in pixels during resize. Overrides per-column defaults when set. |
| maxWidth | number | Infinity | Global maximum column width in pixels during resize. |
| columns | TableColumn<T>[] |  | Column definitions, needed to derive per-column min widths and detect proportional vs pixel columns for neighbor/last-column resize behavior. |

## useTableColumnSettings

Headless column visibility and ordering management for Table. Provides filtered columns, toggle helpers, and pre-built MultiSelector options for a column picker UI.

| Prop | Type | Default | Description |
|---|---|---|---|
| columns * | ColumnSettingsOption[] |  | All available columns with metadata for the settings UI. Each entry has key, label, optional isAlwaysVisible and group. |
| activeColumnKeys * | string[] |  | Currently active column keys, in display order. Only columns with keys in this array are shown. |
| onChangeActiveColumnKeys * | (keys: string[]) => void |  | Called when active columns change (toggle, reorder). |
| defaultColumnKeys | string[] |  | Default column set for "Reset to default". When omitted, reset shows all columns. |

## useTableFilterState

Managed state hook for table filtering. Returns the current filter map and an onChange handler. Pass the result directly to useTableFiltering.

| Prop | Type | Default | Description |
|---|---|---|---|
| initialState | TableFilterState |  | Optional initial filter state map. |

## useTableFiltering

Table plugin that adds inline column filters with popover or inline controls. Pairs with useTableFilterState for managed state. Supports text, select, multi-select, date, and number filter types via PowerSearch field definitions.

| Prop | Type | Default | Description |
|---|---|---|---|
| filters * | TableFilterState |  | Current filter state: map from column key to filter value. |
| onFilterChange * | (columnKey: string, value: TableFilterValue \| null) => void |  | Called when the user changes a filter value. null clears the filter. |
| variant | 'popover' \| 'inline' \| 'inline-compact' | 'popover' | Display variant for filter controls. |

## useTableGroupedRows

Hook that groups a flat data array into collapsible section rows. Each distinct groupBy value becomes a full-width section-header row with a chevron toggle, the group label, and a member count; collapsing hides that group's data rows while keeping the header visible. The disclosure state lives on the chevron button, not the native table row. Mirrors useTableTreeState: the consumer owns the collapsedGroups set and the hook returns {data, plugin, idKey}: pass them to Table as data, plugins, and idKey respectively. Grouping runs on the rows you hand it, so with pagination the order is filter, sort, slice, then group: sort by the group key first and the user's keys second, so a section's rows stay contiguous and each page appends to the bottom of the table instead of splicing rows in above the reader. A page that ends on a row count still cuts mid-section, and the heading then counts what has loaded rather than what exists: "6" quietly becoming "10". Where the full result set is in hand, carry the cut forward to the end of the section it lands in: every rendered section is then whole and its count is a total.

| Prop | Type | Default | Description |
|---|---|---|---|
| data * | T[] |  | The flat data to group. |
| groupBy * | (item: T) => string |  | Derive the group key for a row. Rows with the same key share a section. |
| collapsedGroups * | Set<string> |  | Set of currently-collapsed group keys. |
| onToggleGroup * | (groupKey: string) => void |  | Called with a group key when its header is toggled. |
| renderGroupHeader | (groupKey: string, count: number, collapsed: boolean) => ReactNode |  | Custom renderer for a group header's content (right of the chevron). Defaults to `<groupKey> (<count>)`. |
| getRowKey | (item: T) => string |  | Stable key for a real row. Falls back to a positional key when omitted. |
| groupOrder | string[] |  | Explicit group ordering; groups not listed keep first-seen order after these. |

## useTablePagination

Call useTablePagination with a config object containing page state and callback. Pass the returned plugin to Table via the plugins prop.

| Prop | Type | Default | Description |
|---|---|---|---|
| page * | number |  | Current page number (1-based). |
| onPageChange * | (page: number) => void |  | Called when the page changes. |
| totalItems | number |  | Total number of items across all pages. Used to calculate total page count. |
| totalPages | number |  | Total number of pages. Use when you know the page count but not item count. |
| hasMore | boolean |  | Whether more pages exist. Use for cursor-based pagination where the total is unknown. |
| pageSize | number | 10 | Number of items per page. |
| onPageSizeChange | (pageSize: number) => void |  | Called when the user changes the page size. Shows a page size dropdown when provided with pageSizeOptions. |
| pageSizeOptions | number[] |  | Available page size options. Shows a page size selector when provided. |
| variant | 'pages' \| 'count' \| 'compact' \| 'dots' \| 'none' | 'pages' | Visual variant for the pagination controls. |
| position | 'below' \| 'above' \| 'both' \| 'none' | 'below' | Where to render pagination controls relative to the table. |
| align | 'start' \| 'center' \| 'end' | 'center' | Horizontal alignment of the pagination controls. |
| label | string | 'Table pagination' | Accessible name for the pagination nav landmark. With position='both' the two navs get distinct names (the label suffixed as '(top)' and '(bottom)') so same-type landmarks stay unique (axe landmark-unique). |

## useTableRowExpansion

Hook that returns a TablePlugin which expands a full-width detail panel below a row, rendered by the consumer via renderExpanded(item). Adds a leading chevron column and a right-click "Expand/Collapse row" action; the consumer owns the expandedKeys set. The panel spans the row but starts its content at the first column, past the chevron, so detail lines up with the labels above it; the inset follows the table density. Use it for master-detail rows (order details, forms, charts, nested tables). For hierarchical data where child rows reuse the parent columns, use useTableTreeData + useTableTreeState instead.

| Prop | Type | Default | Description |
|---|---|---|---|
| expandedKeys * | Set<string> |  | Set of currently-expanded row keys. Consumer-owned. |
| onToggle * | (key: string) => void |  | Called with a row key when its expansion is toggled (chevron click or context-menu action). |
| getRowKey * | (item: T) => string |  | Derive a stable unique key from a row item. |
| renderExpanded * | (item: T) => ReactNode |  | Render the detail content shown in a full-width panel below the row when it is expanded. Receives the row item. |
| getIsItemExpandable | (item: T) => boolean |  | Control which rows are expandable. Non-expandable rows show no chevron, no context-menu action, and never render a panel. Defaults to all rows expandable. |
| panelVariant | 'muted' \| 'transparent' | 'transparent' | Background behind the detail panel. 'transparent' (default) paints nothing: the panel is the row's continuation and takes whatever surface the table sits on, so a Card shows through and a striped table's zebra reads on unchanged. 'muted' washes it, marking it as commentary rather than another row of data — reach for it in a bare table with no card, dividers or striping, where nothing else makes that distinction. The wash is a low-alpha near-black, so it is close to invisible over a dark card: 'muted' is largely a light-theme effect. |
| hasRowClickExpansion | boolean | false | Toggle a row by clicking anywhere on it, not only on its chevron. A pointer-only convenience layered over the chevron, which stays the accessible control for keyboard and assistive tech. Clicks on interactive cell content (buttons, links, form controls) or on a text selection do not toggle, and non-expandable rows stay inert. |

### Master-detail order rows

```tsx
const [expandedKeys, setExpandedKeys] = useState(new Set());
const expansion = useTableRowExpansion({
  expandedKeys,
  onToggle: key =>
    setExpandedKeys(prev => {
      const next = new Set(prev);
      next.has(key) ? next.delete(key) : next.add(key);
      return next;
    }),
  getRowKey: item => item.id,
  renderExpanded: item => <OrderDetails order={item} />,
});

<Table data={orders} columns={columns} idKey="id" plugins={{expansion}} />;
```

### Migrating from tree rows to the tree plugin

```tsx
// useTableRowExpansion is now for DETAIL PANELS, not tree rows. If you used
// it to render nested child rows that reuse the parent columns, move to
// useTableTreeData + useTableTreeState.

// BEFORE (tree rows via useTableRowExpansion + useTableRowExpansionState):
const {data, expansionConfig} = useTableRowExpansionState({
  baseData: tree,
  getChildren: item => item.children ?? [],
  getRowKey: item => item.id,
  expandedKeys,
  setExpandedKeys,
});
const expansion = useTableRowExpansion(expansionConfig);
<Table data={data} columns={columns} idKey="id" plugins={{expansion}} />;

// AFTER (tree rows via the tree plugin):
const {visibleData, treeConfig} = useTableTreeState({
  data: tree,                   // nested data
  idKey: 'id',                  // or a function: idKey={item => item.id}
  childrenKey: 'children',      // replaces getChildren
  defaultExpandedIds: ['root'], // or controlled: expandedIds + onExpandedIdsChange
});
const tree = useTableTreeData({
  ...treeConfig,
  hasExpandAllControl: true,    // was isAllExpanded + onToggleExpandAll
  hasRowClickExpansion: true,   // same prop name
});
<Table data={visibleData} columns={columns} idKey="id" plugins={{tree}} />;
```

## useTableRowIndex

Hook that returns a TablePlugin which prepends a right-aligned, monospaced row-number column. Numbering follows the rendered data order (reflecting the current sort / filter / pagination view) and starts at 1 by default. Astryx renderCell receives only the row item, so the plugin takes the rendered data array to derive each ordinal.

| Prop | Type | Default | Description |
|---|---|---|---|
| data * | T[] |  | The data array currently rendered by the table (post sort/filter/page). Numbering follows this order. |
| getRowKey | (item: T) => string |  | Optional key extractor returning a unique string per row. When provided, index lookup is keyed by the returned string; otherwise items are matched by reference identity. Memoize with useCallback for a stable plugin identity. |
| label | ReactNode | '#' | Header label for the index column. |
| startFrom | number | 1 | First index value. |

## useTableRowStatus

Hook that returns a TablePlugin which prepends a narrow column signaling per-row status. Return {status, label} for a semantic success, warning, or error: Table resolves the matching glyph and tone through the active theme. Return {color, icon?, label} for a custom marker: every color is paint-only, an omitted icon renders the stable 8px dot, and an explicit icon renders that caller-selected glyph. Named custom icons keep their released Icon color mapping; raw CSS custom icons inherit the caller's exact paint instead of the previous primary fallback. Existing callers require no source migration. label is required and becomes the accessible image name plus supplemental hover tooltip; return null for no indicator. The column header is visually blank but carries a screen-reader-only localized name ("Row status", i18n key @astryx.table.rowStatus.columnHeader). Memoize getStatus with useCallback for a stable plugin identity.

| Prop | Type | Default | Description |
|---|---|---|---|
| getStatus * | (item: T) => ({ status: 'success' \| 'warning' \| 'error'; color?: never; icon?: never; label: string } \| { status?: never; color: 'accent' \| 'success' \| 'error' \| 'warning' \| 'red' \| 'orange' \| 'green' \| 'yellow' \| 'blue' \| 'gray' \| string; icon?: IconName; label: string }) \| null |  | Derive either a semantic outcome or a custom marker. {status, label} accepts the closed success/error/warning vocabulary and resolves its glyph and tone through the active theme. {color, icon?, label} preserves the stable custom-marker path: color always selects paint, no icon renders an 8px dot, and icon renders the explicit caller glyph. Even color values named success/error/warning remain dots without icon. Valid icon names: close, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, upload, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone. The branches are exclusive, label is required and announced via role="img", and null leaves the row status cell empty. Memoize with useCallback for a stable plugin identity. |

## useTableSelection

Hook that returns a TablePlugin implementing row selection with checkboxes, select-all, and aria-selected. Uses React Context for independent checkbox re-renders.

| Prop | Type | Default | Description |
|---|---|---|---|
| getIsItemSelected * | (item: T) => boolean |  | Returns whether the given item is currently selected. |
| onSelectItem * | (event: {item: T; isSelected: boolean}) => void |  | Called when a row checkbox is toggled. isSelected is the new desired state. |
| onSelectAll * | (event: {isAllSelected: boolean}) => void |  | Called when the select-all header checkbox is toggled. |
| getIsAllSelected * | () => boolean |  | Returns whether all selectable items are currently selected. |
| getIsIndeterminate | () => boolean |  | Returns whether selection is partial (some but not all). Renders the select-all checkbox in indeterminate state. |
| getIsItemSelectable | (item: T) => boolean | () => true | Returns whether a row should show a checkbox. Non-selectable rows render nothing in the selection cell. |
| getIsItemEnabled | (item: T) => boolean | () => true | Returns whether a row checkbox is interactive. Disabled rows show a disabled checkbox. |
| getRowLabel | (item: T) => string |  | Derives a human-readable identity for a row; the row checkbox's hidden label becomes `Select ${getRowLabel(item)}` so screen readers announce which row each checkbox selects. Falls back to "Select row" when omitted. |
| hasRowHighlight | boolean | true | Paints checked rows with the accent wash. Set false when the surrounding UI already uses row background to mean something else (a row open in a detail panel, say). The wash is an inline style, so it cannot be overridden from userland. Only the background is dropped: aria-selected is still set on checked rows either way. |

### Accessible per-row checkbox names

```tsx
const selectionPlugin = useTableSelection({
  getIsItemSelected: item => selectedIds.has(item.id),
  onSelectItem: ({item, isSelected}) => toggle(item.id, isSelected),
  onSelectAll: ({isAllSelected}) => selectAll(isAllSelected),
  getIsAllSelected: () => selectedIds.size === users.length,
  // Screen readers announce "Select Alice", "Select Bob", ...
  // instead of an undifferentiated "Select row" for every checkbox.
  getRowLabel: item => item.name,
});

<Table
  data={users}
  columns={columns}
  idKey="id"
  plugins={{selection: selectionPlugin}}
/>;
```

### Opt out of the checked-row wash

```tsx
// This table already uses row background to mean "open in the detail
// panel". Turning the selection wash off keeps that meaning unambiguous;
// the checkbox and aria-selected still carry the selection.
const selectionPlugin = useTableSelection({
  getIsItemSelected: item => selectedIds.has(item.id),
  onSelectItem: ({item, isSelected}) => toggle(item.id, isSelected),
  onSelectAll: ({isAllSelected}) => selectAll(isAllSelected),
  getIsAllSelected: () => selectedIds.size === users.length,
  hasRowHighlight: false,
});
```

## useTableSelectionState

State management companion for useTableSelection. Returns selectionConfig for the behaviour plugin plus selectionState for selection-aware UI such as TableSelectionToolbar. Handles disabled/selectable row filtering for select-all automatically: disabled rows are frozen (preserved across select-all/deselect-all), non-selectable rows are excluded.

| Prop | Type | Default | Description |
|---|---|---|---|
| data * | T[] |  | The full data array rendered in the table. |
| idKey * | (keyof T & string) \| ((item: T) => string) |  | Key extractor: property name or function returning a unique string ID. |
| selectedKeys * | Set<string> |  | Controlled set of selected item IDs. |
| setSelectedKeys * | Dispatch<SetStateAction<Set<string>>> |  | Setter for the controlled selected keys. |
| getIsItemSelectable | (item: T) => boolean | () => true | Should this row show a checkbox? Non-selectable rows are excluded from select-all. |
| getIsItemEnabled | (item: T) => boolean | () => true | Is this row checkbox interactive? Disabled rows are frozen: select-all preserves their state. |

## useTableSortable

Call useTableSortable with a config object containing sort state and callback. Pass the returned plugin to Table via the plugins prop.

| Prop | Type | Default | Description |
|---|---|---|---|
| sort * | Array<{sortKey: TSortKey, direction: 'ascending' \| 'descending'}> |  | Current sort state (TableSortState<TSortKey>). Ordered array of entries; the first is the primary sort, the rest are tiebreakers. An empty array means unsorted. Direction is the full word: 'ascending' / 'descending', not 'asc' / 'desc'. |
| onSortChange * | (sort: Array<{sortKey: TSortKey, direction: 'ascending' \| 'descending'}>) => void |  | Called when the user clicks a header cell to change sort. Receives the next sort state. |
| allowUnsortedState | boolean | false | Allow cycling back to unsorted. When true: 'ascending', 'descending', unsorted. When false: 'ascending', 'descending', 'ascending'. |
| isMultiSortEnabled | boolean | false | Enable multi-sort via Shift+click. Regular click still replaces the entire sort state. |

## useTableStickyColumns

Hook that returns a TablePlugin which pins a contiguous run of columns to the start and/or end edge of the table. Pinned columns get cumulative offsets and a soft, scroll-aware shadow over the scrolling content. An empty config ({}) is a valid no-op that pins nothing.

| Prop | Type | Default | Description |
|---|---|---|---|
| startKeys | string[] |  | Column keys pinned to the start (inline-start / left in LTR) edge: the contiguous run from the first column through the last listed key. |
| endKeys | string[] |  | Column keys pinned to the end (inline-end / right in LTR) edge: the contiguous run from the first listed key through the last column. |

## useTableTreeData

Headless tree plugin for Table: renders nested rows with per-level indentation and expand/collapse chevrons in the tree column (the first column by default), and reflects hierarchy on body rows via aria-level and aria-expanded. Composable with the other Table plugins: the canonical plugin order places tree before selection, so the checkbox column lands left of the indented tree column. Feed it the treeConfig from useTableTreeState, or construct the config directly for server-driven or pre-flattened trees. When no row is expandable (flat data), every transform is a pass-through and the table renders identically to one without the plugin. Known limitation: the tree column wraps its cell content, so textOverflow="truncate" tooltips do not apply within the tree column.

| Prop | Type | Default | Description |
|---|---|---|---|
| getRowMeta * | (item: T) => TableTreeRowMeta \| undefined |  | Structural meta for a visible row: {id, level (0-based), hasChildren, isExpanded}. |
| onToggleItem * | (item: T) => void |  | Toggle a row's expansion. |
| hasExpandableRows * | boolean |  | Whether any row in the dataset is expandable. When false the plugin is a no-op: no expanders, no indent, no tree ARIA. |
| hasExpandAllControl | boolean | false | Show an expand-all/collapse-all toggle in the tree column header. Requires isAllExpanded plus onExpandAll/onCollapseAll (all supplied by useTableTreeState). |
| isAllExpanded | boolean \| 'indeterminate' |  | Aggregate expansion state across every expandable row, driving the header expand-all toggle. true when all are expanded, false when none are, indeterminate when some are. |
| onExpandAll | () => void |  | Expand every expandable row. Wired to the header control. |
| onCollapseAll | () => void |  | Collapse every row. Wired to the header control. |
| indent | 'sm' \| 'md' \| 'lg' | 'md' | Indent step per level, mapped to the spacing-3 / spacing-4 / spacing-6 tokens. |
| treeColumnKey | string |  | Column that carries the indent + expander. Defaults to the first column. |
| hasRowClickExpansion | boolean | false | When true, clicking anywhere on an expandable row toggles its expansion, in addition to the chevron. A pointer-only convenience: keyboard and assistive-tech users toggle via the chevron button. Clicks on interactive cell content (buttons, links, form controls) or a text selection do not toggle. Leaf rows stay inert, and it is a no-op on flat data. |

### File tree with expandable folders

```tsx
const {visibleData, treeConfig} = useTableTreeState({
  data: files,          // rows may nest under 'children'
  idKey: 'id',
  defaultExpandedIds: ['src'],
});
const tree = useTableTreeData(treeConfig);

<Table data={visibleData} columns={columns} idKey="id" plugins={{tree}} />;
```

## useTableTreeState

State management companion for useTableTreeData. Owns the expanded set (controlled or uncontrolled) and flattens nested data into the visible row array; collapsed subtrees are unmounted, not hidden, so the table body contains exactly the visible rows. Returns expandAll/collapseAll helpers, the aggregate isAllExpanded state (true/false/indeterminate) for a header expand-all control, and a ready-to-use config for the tree plugin. Note: because collapsed rows unmount, cell-local React state inside collapsed subtrees is lost on collapse; lift state that must survive.

| Prop | Type | Default | Description |
|---|---|---|---|
| data * | T[] |  | Nested data: rows may carry child rows under childrenKey. Flat data (no children anywhere) makes the plugin a no-op, so it can be adopted before the data becomes hierarchical. |
| idKey * | (keyof T & string) \| ((item: T) => string \| number) |  | Row ID accessor: property name or function returning a unique id. |
| childrenKey | string | 'children' | Property holding each row's children array. |
| defaultExpandedIds | Iterable<string> |  | Initial expanded row ids for uncontrolled mode. Ignored when expandedIds is provided. |
| expandedIds | ReadonlySet<string> |  | Controlled set of expanded row ids. Pair with onExpandedIdsChange. |
| onExpandedIdsChange | (ids: ReadonlySet<string>) => void |  | Called with the next expanded set whenever expansion changes (both modes). In lazy-loading setups, trigger the children fetch here. |
| isItemExpandable | (item: T) => boolean |  | Should this row show an expander? Overrides the default non-empty-children check; use for lazy loading, where a row is expandable before its children have been fetched. |
| sortSiblings | (siblings: T[]) => T[] |  | Sort each sibling group independently during flattening; children always stay directly under their parent. Pass applySort from useTableSortableState to compose with column sorting. |
| indent | 'sm' \| 'md' \| 'lg' | 'md' | Indent step per level (spacing-3 / spacing-4 / spacing-6), forwarded to useTableTreeData. |
| treeColumnKey | string |  | Column that carries the indent + expander, forwarded to useTableTreeData. Defaults to the first column. |


## Examples

- Table: Data-driven table with proportional and pixel column widths and hover highlighting.
- useTableColumnResize — Draggable Columns: A Table using useTableColumnResize. Drag the right edge of any column header to resize; widths are committed on release. The last proportional column flexes to fill remaining space.
- useTableStickyColumns — Pinned Columns: A Table using useTableStickyColumns to pin the Name column to the start edge and Status to the end edge. Scroll horizontally; pinned columns stay in view with a soft shadow over the scrolling content.
- Table: Bulk Actions: Controlled row selection with the reusable TableSelectionToolbar rendered in flow above the table. Product actions occupy the logical start; synchronized selection count and complete clear action occupy the logical end.
- Table — Column Settings: Table with a column visibility picker in the toolbar. Toggle columns on and off.
- Table — Popover Filters: Table with popover filter controls triggered by icons in column headers.
- Table: Floating Bulk Actions: The reusable TableSelectionToolbar inside a capped, self-contained scroll region for a long non-sticky table below metric cards. The caller-owned sticky placement keeps actions 16px from the example scrollport without moving the table when selection changes.
- Table — Grid Dividers: Compact table with grid dividers showing both row and column borders, suited for dense numeric data.
- useTableGroupedRows — Collapsible Groups: A table grouped into collapsible sections with useTableGroupedRows. Each group gets a full-width header with a chevron, label, and member count; click to collapse/expand.
- Table — In Card: Table composed inside a card with a heading, demonstrating container bleed alignment.
- Table — Inline Filters: Table with inline filter controls rendered directly below each column header.
- Table — Paginated Data: Paginated data table navigating through a larger dataset page by page.
- Table — Resizable Columns: Table with draggable column resize handles. Drag the right edge of any header to resize.
- Table — Rich Cell Content: Table with rich cell content using Link for emails and Badge for role labels.
- useTableRowExpansion — Tree Table: A tree table using useTableRowExpansion with inherited columns. Child rows use the same columns as parents, indented by depth. Click the chevron or right-click to expand/collapse.
- useTableRowIndex — Numbered Rows: A table with a prepended row-number column via useTableRowIndex. Numbering is monospaced, right-aligned, and follows the rendered data order.
- useTableRowStatus - Semantic and Custom Markers: A job table using both row-status paths: status resolves themed semantic glyphs and tones, while color stays a custom paint choice with an optional caller-selected icon or the stable dot.
- Table — Row Selection: Table with row selection checkboxes and a select-all header checkbox.
- Table — Sortable Columns: Table with sortable columns, click headers to sort ascending or descending.
- Table — Striped Rows: Table with alternating row colors and hover highlighting for easy scanning.
- useTableTreeData: Tree Table: A file-tree table built from nested data. useTableTreeState flattens the tree into the visible rows and owns the expanded set; useTableTreeData draws the per-level indent and the expand/collapse chevron in the tree column. The two hooks are designed to work together, so this one example covers both. hasExpandAllControl adds the expand-all/collapse-all toggle to the tree column header. Collapsed branches are unmounted, not hidden.

## Source StyleX compilation

TableFloatingBulkActionsTable contain stylex.create and require a StyleX source build pipeline. Precompiled Core CSS alone does not compile these examples. See [Styling](/resources/astryx/docs/styling) before copying them.

---

Index of all JGuang resources: https://jiguang132.com/llms.txt
