docs(table): rewrite Table component documentation

Comprehensive rewrite of the Table component documentation including:

- Updated prop definitions with JSX descriptions
- New interactive examples for sorting, search, pagination, selection
- Expanded code snippets covering all major use cases
- Added custom row and primitives examples
- Improved MDX structure and styling

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-01-22 12:23:11 +01:00
parent 40a30551a0
commit ec66afb03e
7 changed files with 1546 additions and 565 deletions
+460 -94
View File
@@ -1,31 +1,46 @@
'use client';
import { useState } from 'react';
import {
Table,
TableRoot,
TableHeader,
TableBody,
Column,
Row,
CellProfile,
CellText,
type ColumnConfig,
useTable,
type ColumnConfig,
} from '../../../../../packages/ui/src/components/Table';
import { Flex } from '../../../../../packages/ui/src/components/Flex/Flex';
import { Text } from '../../../../../packages/ui/src/components/Text/Text';
import { RadioGroup } from '../../../../../packages/ui/src/components/RadioGroup/RadioGroup';
import { Radio } from '../../../../../packages/ui/src/components/RadioGroup';
import { data as data4 } from '../../../../../packages/ui/src/components/Table/stories/mocked-data4';
import { useState } from 'react';
import { SearchField } from '../../../../../packages/ui/src/components/SearchField/SearchField';
import {
selectionData,
selectionColumns,
} from '../../../../../packages/ui/src/components/Table/stories/utils';
RadioGroup,
Radio,
} from '../../../../../packages/ui/src/components/RadioGroup';
import { data as rockBandData } from '../../../../../packages/ui/src/components/Table/stories/mocked-data4';
import { data as catalogData } from '../../../../../packages/ui/src/components/Table/stories/mocked-data1';
import { MemoryRouter } from 'react-router-dom';
type Data4Item = (typeof data4)[0];
// =============================================================================
// Types
// =============================================================================
const columns: ColumnConfig<Data4Item>[] = [
type RockBandItem = (typeof rockBandData)[0];
type CatalogItem = (typeof catalogData)[0];
// =============================================================================
// Hero Example
// =============================================================================
const heroColumns: ColumnConfig<RockBandItem>[] = [
{
id: 'name',
label: 'Band name',
isRowHeader: true,
defaultWidth: '3fr',
cell: item => (
<CellProfile name={item.name} src={item.image} href={item.website} />
),
@@ -33,112 +48,183 @@ const columns: ColumnConfig<Data4Item>[] = [
{
id: 'genre',
label: 'Genre',
defaultWidth: '3fr',
cell: item => <CellText title={item.genre} />,
},
{
id: 'yearFormed',
label: 'Year formed',
defaultWidth: '1fr',
cell: item => <CellText title={item.yearFormed.toString()} />,
},
{
id: 'albums',
label: 'Albums',
defaultWidth: '1fr',
cell: item => <CellText title={item.albums.toString()} />,
},
];
export const TableRockBand = () => {
export function HeroExample() {
const { tableProps } = useTable({
mode: 'complete',
getData: () => data4,
getData: () => rockBandData,
paginationOptions: { pageSize: 5 },
});
return (
<MemoryRouter>
<Table columnConfig={columns} {...tableProps} />
<Table columnConfig={heroColumns} {...tableProps} />
</MemoryRouter>
);
};
}
export const SelectionToggleWithActions = () => {
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
new Set(),
);
// =============================================================================
// Sorting Example
// =============================================================================
const sortingColumns: ColumnConfig<CatalogItem>[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
isSortable: true,
cell: item => <CellText title={item.name} />,
},
{
id: 'owner',
label: 'Owner',
isSortable: true,
cell: item => <CellText title={item.owner.name} />,
},
{
id: 'type',
label: 'Type',
isSortable: true,
cell: item => <CellText title={item.type} />,
},
{
id: 'lifecycle',
label: 'Lifecycle',
isSortable: true,
cell: item => <CellText title={item.lifecycle} />,
},
];
export function SortingExample() {
const { tableProps } = useTable({
mode: 'complete',
getData: () => selectionData,
paginationOptions: { pageSize: 10 },
getData: () => catalogData,
paginationOptions: { pageSize: 5 },
initialSort: { column: 'name', direction: 'ascending' },
sortFn: (items, { column, direction }) => {
return [...items].sort((a, b) => {
let aVal: string;
let bVal: string;
if (column === 'owner') {
aVal = a.owner.name;
bVal = b.owner.name;
} else {
aVal = String(a[column as keyof CatalogItem]);
bVal = String(b[column as keyof CatalogItem]);
}
const cmp = aVal.localeCompare(bVal);
return direction === 'descending' ? -cmp : cmp;
});
},
});
return (
<MemoryRouter>
<Table
{...tableProps}
columnConfig={selectionColumns}
selection={{
mode: 'multiple',
behavior: 'toggle',
selected,
onSelectionChange: setSelected,
}}
rowConfig={{ onClick: item => alert(`Clicked: ${item.name}`) }}
/>
<Table columnConfig={sortingColumns} {...tableProps} />
</MemoryRouter>
);
};
}
export const SelectionModePlayground = () => {
const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
'multiple',
);
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
new Set(),
);
// =============================================================================
// Search Example
// =============================================================================
const { tableProps } = useTable({
const searchColumns: ColumnConfig<CatalogItem>[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
cell: item => <CellText title={item.name} description={item.description} />,
},
{
id: 'owner',
label: 'Owner',
cell: item => <CellText title={item.owner.name} />,
},
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
];
export function SearchExample() {
const { tableProps, search } = useTable({
mode: 'complete',
getData: () => selectionData,
paginationOptions: { pageSize: 10 },
getData: () => catalogData,
paginationOptions: { pageSize: 5 },
searchFn: (items, query) => {
const lowerQuery = query.toLocaleLowerCase('en-US');
return items.filter(
item =>
item.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
item.owner.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
item.type.toLocaleLowerCase('en-US').includes(lowerQuery),
);
},
});
return (
<MemoryRouter>
<Flex direction="column" gap="8">
<Table
{...tableProps}
columnConfig={selectionColumns}
selection={{
mode: selectionMode,
behavior: 'toggle',
selected,
onSelectionChange: setSelected,
}}
<Flex direction="column" gap="4">
<SearchField
aria-label="Search"
placeholder="Search..."
value={search.value}
onChange={search.onChange}
style={{ maxWidth: 300 }}
/>
<Table
columnConfig={searchColumns}
emptyState={
search.value ? (
<Text>No results found for &quot;{search.value}&quot;</Text>
) : (
<Text>No data available</Text>
)
}
{...tableProps}
/>
<div>
<Text as="h4" style={{ marginBottom: 'var(--bui-space-2)' }}>
Selection mode:
</Text>
<RadioGroup
aria-label="Selection mode"
orientation="horizontal"
value={selectionMode}
onChange={value => {
setSelectionMode(value as 'single' | 'multiple');
setSelected(new Set());
}}
>
<Radio value="single">single</Radio>
<Radio value="multiple">multiple</Radio>
</RadioGroup>
</div>
</Flex>
</MemoryRouter>
);
};
}
export const SelectionBehaviorPlayground = () => {
// =============================================================================
// Selection Example
// =============================================================================
const selectionColumns: ColumnConfig<CatalogItem>[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
cell: item => <CellText title={item.name} />,
},
{
id: 'owner',
label: 'Owner',
cell: item => <CellText title={item.owner.name} />,
},
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
];
export function SelectionExample() {
const [selectionMode, setSelectionMode] = useState<'single' | 'multiple'>(
'multiple',
);
const [selectionBehavior, setSelectionBehavior] = useState<
'toggle' | 'replace'
>('toggle');
@@ -148,41 +234,321 @@ export const SelectionBehaviorPlayground = () => {
const { tableProps } = useTable({
mode: 'complete',
getData: () => selectionData,
paginationOptions: { pageSize: 10 },
getData: () => catalogData.slice(0, 5),
});
return (
<MemoryRouter>
<Flex direction="column" gap="8">
<Flex direction="column" gap="4">
<Table
{...tableProps}
columnConfig={selectionColumns}
selection={{
mode: 'multiple',
mode: selectionMode,
behavior: selectionBehavior,
selected,
onSelectionChange: setSelected,
}}
{...tableProps}
/>
<div>
<Text as="h4" style={{ marginBottom: 'var(--bui-space-2)' }}>
Selection behavior:
</Text>
<RadioGroup
aria-label="Selection behavior"
orientation="horizontal"
value={selectionBehavior}
onChange={value => {
setSelectionBehavior(value as 'toggle' | 'replace');
setSelected(new Set());
}}
>
<Radio value="toggle">toggle</Radio>
<Radio value="replace">replace</Radio>
</RadioGroup>
</div>
<Flex gap="8">
<div>
<Text as="p" style={{ marginBottom: 'var(--bui-space-2)' }}>
Selection mode:
</Text>
<RadioGroup
aria-label="Selection mode"
orientation="horizontal"
value={selectionMode}
onChange={value => {
setSelectionMode(value as 'single' | 'multiple');
setSelected(new Set());
}}
>
<Radio value="single">single</Radio>
<Radio value="multiple">multiple</Radio>
</RadioGroup>
</div>
<div>
<Text as="p" style={{ marginBottom: 'var(--bui-space-2)' }}>
Selection behavior:
</Text>
<RadioGroup
aria-label="Selection behavior"
orientation="horizontal"
value={selectionBehavior}
onChange={value => {
setSelectionBehavior(value as 'toggle' | 'replace');
setSelected(new Set());
}}
>
<Radio value="toggle">toggle</Radio>
<Radio value="replace">replace</Radio>
</RadioGroup>
</div>
</Flex>
</Flex>
</MemoryRouter>
);
};
}
// =============================================================================
// Row Actions Example
// =============================================================================
export function RowActionsExample() {
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
new Set(),
);
const { tableProps } = useTable({
mode: 'complete',
getData: () => catalogData.slice(0, 5),
});
return (
<MemoryRouter>
<Table
columnConfig={selectionColumns}
rowConfig={{
onClick: item => alert(`Clicked: ${item.name}`),
}}
selection={{
mode: 'multiple',
behavior: 'toggle',
selected,
onSelectionChange: setSelected,
}}
{...tableProps}
/>
</MemoryRouter>
);
}
// =============================================================================
// Empty State Example
// =============================================================================
const emptyStateColumns: ColumnConfig<CatalogItem>[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
cell: item => <CellText title={item.name} />,
},
{
id: 'owner',
label: 'Owner',
cell: item => <CellText title={item.owner.name} />,
},
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
];
export function EmptyStateExample() {
const emptyData: CatalogItem[] = [];
const { tableProps } = useTable({
mode: 'complete',
getData: () => emptyData,
});
return (
<MemoryRouter>
<Table
columnConfig={emptyStateColumns}
emptyState={<Text>No items yet. Create one to get started.</Text>}
{...tableProps}
/>
</MemoryRouter>
);
}
// =============================================================================
// Combined Example
// =============================================================================
const combinedColumns: ColumnConfig<CatalogItem>[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
isSortable: true,
cell: item => <CellText title={item.name} description={item.description} />,
},
{
id: 'owner',
label: 'Owner',
isSortable: true,
cell: item => <CellText title={item.owner.name} />,
},
{
id: 'type',
label: 'Type',
isSortable: true,
cell: item => <CellText title={item.type} />,
},
];
export function CombinedExample() {
const [selected, setSelected] = useState<Set<string | number> | 'all'>(
new Set(),
);
const { tableProps, search } = useTable({
mode: 'offset',
getData: async ({ offset, pageSize, search: query, sort }) => {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Filter data
let filtered = catalogData;
if (query) {
const lowerQuery = query.toLocaleLowerCase('en-US');
filtered = filtered.filter(
item =>
item.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
item.owner.name.toLocaleLowerCase('en-US').includes(lowerQuery) ||
item.type.toLocaleLowerCase('en-US').includes(lowerQuery),
);
}
// Sort data
if (sort?.column) {
filtered = [...filtered].sort((a, b) => {
let aVal: string;
let bVal: string;
if (sort.column === 'owner') {
aVal = a.owner.name;
bVal = b.owner.name;
} else {
aVal = String(a[sort.column as keyof CatalogItem]);
bVal = String(b[sort.column as keyof CatalogItem]);
}
const cmp = aVal.localeCompare(bVal);
return sort.direction === 'descending' ? -cmp : cmp;
});
}
// Paginate
const data = filtered.slice(offset, offset + pageSize);
return { data, totalCount: filtered.length };
},
paginationOptions: { pageSize: 5, pageSizeOptions: [5, 10, 20] },
initialSort: { column: 'name', direction: 'ascending' },
});
return (
<MemoryRouter>
<Flex direction="column" gap="4">
<SearchField
aria-label="Search"
placeholder="Search..."
value={search.value}
onChange={search.onChange}
style={{ maxWidth: 300 }}
/>
<Table
columnConfig={combinedColumns}
rowConfig={{
onClick: item => alert(`Clicked: ${item.name}`),
}}
selection={{
mode: 'multiple',
behavior: 'toggle',
selected,
onSelectionChange: setSelected,
}}
emptyState={
search.value ? (
<Text>No results match your search</Text>
) : (
<Text>No items available</Text>
)
}
{...tableProps}
/>
</Flex>
</MemoryRouter>
);
}
// =============================================================================
// Custom Row Example
// =============================================================================
const customRowColumns: ColumnConfig<CatalogItem>[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
cell: item => <CellText title={item.name} />,
},
{
id: 'owner',
label: 'Owner',
cell: item => <CellText title={item.owner.name} />,
},
{
id: 'lifecycle',
label: 'Lifecycle',
cell: item => <CellText title={item.lifecycle} />,
},
];
export function CustomRowExample() {
const { tableProps } = useTable({
mode: 'complete',
getData: () => catalogData.slice(0, 5),
});
return (
<MemoryRouter>
<Table
columnConfig={customRowColumns}
rowConfig={({ item }) => (
<Row
id={String(item.id)}
columns={customRowColumns}
style={
item.lifecycle === 'experimental'
? { backgroundColor: 'var(--bui-bg-warning)' }
: undefined
}
>
{column => column.cell(item)}
</Row>
)}
{...tableProps}
/>
</MemoryRouter>
);
}
// =============================================================================
// Primitives Example
// =============================================================================
export function PrimitivesExample() {
const items = catalogData.slice(0, 5);
return (
<MemoryRouter>
<TableRoot>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Owner</Column>
<Column>Type</Column>
</TableHeader>
<TableBody>
{items.map(item => (
<Row key={item.id} id={String(item.id)}>
<CellText title={item.name} />
<CellText title={item.owner.name} />
<CellText title={item.type} />
</Row>
))}
</TableBody>
</TableRoot>
</MemoryRouter>
);
}
+239 -106
View File
@@ -2,161 +2,294 @@ import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import {
TableRockBand,
SelectionModePlayground,
SelectionBehaviorPlayground,
SelectionToggleWithActions,
HeroExample,
SortingExample,
SearchExample,
SelectionExample,
RowActionsExample,
EmptyStateExample,
CombinedExample,
CustomRowExample,
PrimitivesExample,
} from './components';
import {
tablePropsColumns,
tableReturnColumns,
useTableOptionsPropDefs,
useTableReturnPropDefs,
tablePropDefs,
tableHeaderPropDefs,
tableBodyPropDefs,
tablePaginationPropDefs,
columnConfigPropDefs,
cellTextPropDefs,
cellProfilePropDefs,
tableRootPropDefs,
columnPropDefs,
rowPropDefs,
cellPropDefs,
} from './props-definition';
import {
tableUsageSnippet,
tableBasicSnippet,
tableSelectionActionsSnippet,
tableSelectionModeSnippet,
tableSelectionBehaviorSnippet,
tableHeroSnippet,
tableConceptsSnippet,
tableSortingSnippet,
tablePaginationSnippet,
tableSearchSnippet,
tableSelectionSnippet,
tableRowActionsHrefSnippet,
tableRowActionsClickSnippet,
tableRowActionsDisabledSnippet,
tableEmptyStateSnippet,
tableOffsetPaginationSnippet,
tableCursorPaginationSnippet,
tableCombinedSnippet,
tableCustomRowSnippet,
tablePrimitivesSnippet,
} from './snippets';
import { ChangelogComponent } from '@/components/ChangelogComponent';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ReactAriaLink } from '@/components/ReactAriaLink';
import { TableDefinition } from '../../../utils/definitions';
export const reactAriaUrls = {
table: 'https://react-aria.adobe.com/Table#table',
tableHeader: 'https://react-aria.adobe.com/Table#tableheader',
tableBody: 'https://react-aria.adobe.com/Table#tablebody',
column: 'https://react-aria.adobe.com/Table#column',
row: 'https://react-aria.adobe.com/Table#row',
cell: 'https://react-aria.adobe.com/Table#cell',
};
<PageTitle
title="Table"
description="A flexible table component built on top of TanStack Table with built-in styling, interactions, and pagination support."
description="A data table component with built-in pagination, sorting, and selection."
/>
<Snippet py={4} preview={<TableRockBand />} code={tableBasicSnippet} />
<Snippet py={4} preview={<HeroExample />} code={tableHeroSnippet} />
## Usage
<CodeBlock code={tableUsageSnippet} />
## API reference
## Core Concepts
### Table
The Table component is designed around a hook + component pattern:
The main table component that renders data in a structured grid format.
- **`useTable`** manages data fetching, pagination state, sorting, and filtering. It returns `tableProps` that you spread onto the Table component.
- **`Table`** handles rendering - columns, rows, cells, and interactions.
<PropsTable data={tablePropDefs} />
The hook supports three modes for different data scenarios:
### TableHeader
- **`complete`** - You have all data available upfront (client-side)
- **`offset`** - Your API uses offset/limit pagination
- **`cursor`** - Your API uses cursor-based pagination
The header section of the table that contains the column definitions.
<CodeBlock code={tableConceptsSnippet} />
<PropsTable data={tableHeaderPropDefs} />
**Controlled vs uncontrolled state**
### Column
By default, `useTable` manages sort, search, and filter state internally. Pass `initialSort`, `initialSearch`, or `initialFilter` to set starting values.
A column definition that describes how a column should be rendered.
For full control over state, use the controlled props instead:
<PropsTable data={columnPropDefs} />
- `sort` / `onSortChange`
- `search` / `onSearchChange`
- `filter` / `onFilterChange`
### TableBody
The body section of the table that contains the rows.
<PropsTable data={tableBodyPropDefs} />
### Row
A row definition that describes how a row should be rendered.
<PropsTable data={rowPropDefs} />
### Cell
A cell definition that describes how a cell should be rendered.
<PropsTable data={cellPropDefs} />
### TablePagination
A pagination component designed to work with the Table component.
<PropsTable data={tablePaginationPropDefs} />
## Examples
### Basic Table
Coming soon.
### Row selection
Tables support row selection with two configuration options: selection mode and selection behavior.
#### Selection mode
Use `selectionMode` to control how many rows can be selected. With `single`, only one row can be selected at a time. With `multiple`, any number of rows can be selected, and a header checkbox provides select-all functionality.
<Snippet
preview={<SelectionModePlayground />}
code={tableSelectionModeSnippet}
/>
#### Selection behavior
Use `selectionBehavior` to control how selection is indicated and interacted with. With `toggle`, checkboxes appear for selection. With `replace`, selection is indicated by row background color—click to select, Cmd/Ctrl+click for multiple.
<Snippet
preview={<SelectionBehaviorPlayground />}
code={tableSelectionBehaviorSnippet}
/>
#### With row actions
With toggle behavior, clicking a row triggers its action when nothing is selected. Once any row is selected, clicking toggles selection instead.
With replace behavior, clicking selects the row and double-clicking triggers the action.
<Snippet
preview={<SelectionToggleWithActions />}
code={tableSelectionActionsSnippet}
/>
### Row Clicks
Coming soon.
### Pagination
Coming soon.
## Common Patterns
### Sorting
Coming soon.
Columns can be made sortable by adding `isSortable: true` to the column configuration. When sortable, clicking a column header cycles through ascending, descending, and unsorted states. A visual indicator shows the current sort direction.
### Asynchronous loading
With `mode: 'complete'`, sorting happens client-side. Provide a `sortFn` that receives the full dataset and the current sort descriptor, and returns the sorted array. You can also set an `initialSort` to define the default sort when the table first renders. For server-side sorting with `offset` or `cursor` modes, see [Server-Side Data](#server-side-data).
Coming soon.
<Snippet preview={<SortingExample />} code={tableSortingSnippet} />
### Empty state
### Pagination
Coming soon.
Configure page size and available options through `paginationOptions`. The table displays navigation controls automatically.
### Column resizing
<CodeBlock code={tablePaginationSnippet} />
This feature is not available yet — let us know if you'd like us to explore it!
### Search
### Column reordering
The `useTable` hook returns a `search` object with `value` and `onChange` properties, ready to connect to a search input. With `mode: 'complete'`, provide a `searchFn` that filters the dataset based on the search query.
This feature is not available yet — let us know if you'd like us to explore it!
The search state is debounced internally, so rapid typing doesn't trigger excessive re-filtering. When the search query changes, pagination resets to the first page automatically.
### Column pinning
For server-side search with `offset` or `cursor` modes, the search query is passed to your `getData` function. See [Server-Side Data](#server-side-data).
This feature is not available yet — let us know if you'd like us to explore it!
<Snippet preview={<SearchExample />} code={tableSearchSnippet} />
### Column visibility
### Row Selection
This feature is not available yet — let us know if you'd like us to explore it!
Tables support row selection with two configuration options: `mode` and `behavior`.
Selection mode controls how many rows can be selected:
- `single` - Only one row can be selected at a time
- `multiple` - Any number of rows can be selected, with a header checkbox for select-all
Selection behavior controls the interaction style:
- `toggle` - Checkboxes appear for selection. Click a checkbox to select/deselect.
- `replace` - No checkboxes. Click a row to select it (replacing previous selection). Use Cmd/Ctrl+click to select multiple rows.
Selection state can be managed in two ways:
- **Controlled** - Pass both `selected` and `onSelectionChange` to fully manage state externally
- **Uncontrolled** - Pass only `onSelectionChange` to let the Table manage state while receiving change notifications
<Snippet preview={<SelectionExample />} code={tableSelectionSnippet} />
### Row Actions
Rows can respond to user interaction in two ways: navigating to a URL or triggering a callback.
Use `getHref` when rows should link to detail pages. The entire row becomes clickable and navigates on click. Links within cells remain independently clickable.
<CodeBlock code={tableRowActionsHrefSnippet} />
Use `onClick` for custom actions like opening a panel or triggering a dialog.
<CodeBlock code={tableRowActionsClickSnippet} />
When combining row actions with selection, the interaction depends on selection behavior:
- **`toggle`**: Clicking a row triggers its action when nothing is selected. Once any row is selected, clicking anywhere on the row toggles selection instead.
- **`replace`**: Single click selects the row. Double-click triggers the row action.
You can also disable specific rows from being clicked using `getIsDisabled`:
<CodeBlock code={tableRowActionsDisabledSnippet} />
<Snippet preview={<RowActionsExample />} />
### Empty State
When the table has no data to display, provide an `emptyState` to show a helpful message instead of an empty table body. Consider providing different messages depending on context - an empty search result is different from a table with no data at all.
<Snippet preview={<EmptyStateExample />} code={tableEmptyStateSnippet} />
## Server-Side Data
When your data comes from an API with server-side pagination, use `offset` or `cursor` mode instead of `complete`. The key difference: instead of providing all data upfront, you provide a `getData` function that fetches data for the current page.
### Offset Pagination
Use `mode: 'offset'` when your API accepts `offset` and `limit` (or `skip` and `take`) parameters.
<CodeBlock code={tableOffsetPaginationSnippet} />
The `signal` parameter is an `AbortSignal` - pass it to `fetch` so in-flight requests are cancelled when the user navigates away or triggers a new query.
### Cursor Pagination
Use `mode: 'cursor'` when your API uses cursor-based pagination (common with GraphQL or APIs that return `nextCursor`/`prevCursor` tokens).
<CodeBlock code={tableCursorPaginationSnippet} />
### Loading States
When fetching data, the table shows a loading state. If the user triggers a new query (by paginating, sorting, or searching) while previous data is displayed, the table enters a "stale" state where it continues showing the previous data until new data arrives. This prevents jarring layout shifts.
You can access these states via `tableProps.loading` and `tableProps.isStale` if you need to render additional loading indicators.
## Combining Features
Real-world tables often combine multiple features. Here's an example with search, sorting, pagination, and row selection working together.
<Snippet preview={<CombinedExample />} code={tableCombinedSnippet} />
## Custom Tables
For most use cases, `useTable` + `Table` provides everything you need. When you need more control, there are several ways you can customize your table.
### Custom Row and Header Rendering
Use a render function for `rowConfig` when you need to customize individual rows based on their data - for example, highlighting rows that require attention. Use `header` in column config to customize header cell content.
<Snippet preview={<CustomRowExample />} code={tableCustomRowSnippet} />
### Using Primitives Directly
When the `Table` component doesn't support your use case, you can compose with the lower-level primitives: `TableRoot`, `TableHeader`, `TableBody`, `Column`, and `Row`.
<Snippet preview={<PrimitivesExample />} code={tablePrimitivesSnippet} />
## API Reference
### useTable
The `useTable` hook manages data fetching, pagination, sorting, and filtering.
**Options**
<PropsTable data={useTableOptionsPropDefs} columns={tablePropsColumns} />
**Return Value**
<PropsTable data={useTableReturnPropDefs} columns={tableReturnColumns} />
### Table
The main table component.
<PropsTable data={tablePropDefs} columns={tablePropsColumns} />
### ColumnConfig
<PropsTable data={columnConfigPropDefs} columns={tablePropsColumns} />
### CellText
<PropsTable data={cellTextPropDefs} columns={tablePropsColumns} />
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
### CellProfile
<PropsTable data={cellProfilePropDefs} columns={tablePropsColumns} />
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
### TablePagination
<PropsTable data={tablePaginationPropDefs} columns={tablePropsColumns} />
### Primitives
Low-level components for building custom table layouts.
#### TableRoot
<PropsTable data={tableRootPropDefs} columns={tablePropsColumns} />
<ReactAriaLink component="Table" href={reactAriaUrls.table} />
#### TableHeader
<ReactAriaLink component="TableHeader" href={reactAriaUrls.tableHeader} />
#### TableBody
<ReactAriaLink component="TableBody" href={reactAriaUrls.tableBody} />
#### Column
<PropsTable data={columnPropDefs} columns={tablePropsColumns} />
<ReactAriaLink component="Column" href={reactAriaUrls.column} />
#### Row
<PropsTable data={rowPropDefs} columns={tablePropsColumns} />
<ReactAriaLink component="Row" href={reactAriaUrls.row} />
#### Cell
<ReactAriaLink component="Cell" href={reactAriaUrls.cell} />
<Theming definition={TableDefinition} />
@@ -1,268 +0,0 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const tablePropDefs: Record<string, PropDef> = {
selectionBehavior: {
type: 'enum',
values: ['toggle', 'replace'],
default: 'toggle',
description: 'How multiple selection should behave in the collection.',
},
disabledBehavior: {
type: 'enum',
values: ['selection', 'all'],
default: 'selection',
description:
'Whether disabledKeys applies to all interactions, or only selection.',
},
disabledKeys: {
type: 'enum',
values: ['Iterable<Key>'],
description: 'A list of row keys to disable.',
},
selectionMode: {
type: 'enum',
values: ['single', 'multiple'],
description: 'The type of selection that is allowed in the collection.',
},
selectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
description: 'The currently selected keys in the collection (controlled).',
},
defaultSelectedKeys: {
type: 'enum',
values: ['all', 'Iterable<Key>'],
description: 'The initial selected keys in the collection (uncontrolled).',
},
onRowAction: {
type: 'enum',
values: ['(key: Key) => void'],
description:
'Handler that is called when a user performs an action on the row.',
},
onSelectionChange: {
type: 'enum',
values: ['(keys: Selection) => void'],
description: 'Handler that is called when the selection changes.',
},
onSortChange: {
type: 'enum',
values: ['(descriptor: SortDescriptor) => any'],
description:
'Handler that is called when the sorted column or direction changes.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const tableHeaderPropDefs: Record<string, PropDef> = {
onHoverStart: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
description: 'Handler that is called when a hover interaction starts.',
},
onHoverEnd: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
description: 'Handler that is called when a hover interaction ends.',
},
onHoverChange: {
type: 'enum',
values: ['(isHovering: boolean) => void'],
description: 'Handler that is called when the hover state changes.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const columnPropDefs: Record<string, PropDef> = {
id: {
type: 'enum',
values: ['Key'],
description: 'The unique id of the column.',
},
allowsSorting: {
type: 'boolean',
description: 'Whether the column allows sorting.',
},
isRowHeader: {
type: 'boolean',
description:
'Whether a column is a row header and should be announced by assistive technology during row navigation.',
},
textValue: {
type: 'string',
description:
"A string representation of the column's contents, used for accessibility announcements.",
},
...classNamePropDefs,
...stylePropDefs,
};
export const tableBodyPropDefs: Record<string, PropDef> = {
renderEmptyState: {
type: 'enum',
values: ['(props) => ReactNode'],
description:
'Provides content to display when there are no rows in the table.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const rowPropDefs: Record<string, PropDef> = {
textValue: {
type: 'string',
description:
"A string representation of the row's contents, used for accessibility announcements.",
},
isDisabled: {
type: 'boolean',
description: 'Whether the row is disabled.',
},
id: {
type: 'enum',
values: ['Key'],
description: 'The unique id of the row.',
},
href: {
type: 'string',
description: 'The URL to navigate to when the row is clicked.',
},
hrefLang: {
type: 'string',
description:
'The language of the URL to navigate to when the row is clicked.',
},
target: {
type: 'string',
description:
'The target of the URL to navigate to when the row is clicked.',
},
rel: {
type: 'string',
description:
'The relationship of the URL to navigate to when the row is clicked.',
},
onAction: {
type: 'enum',
values: ['() => void'],
description:
"Handler that is called when a user performs an action on the row. The exact user event depends on the collection's selectionBehavior prop and the interaction modality.",
},
onHoverStart: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
description: 'Handler that is called when a hover interaction starts.',
},
onHoverEnd: {
type: 'enum',
values: ['(e: HoverEvent) => void'],
description: 'Handler that is called when a hover interaction ends.',
},
onHoverChange: {
type: 'enum',
values: ['(isHovering: boolean) => void'],
description: 'Handler that is called when the hover state changes.',
},
onPress: {
type: 'enum',
values: ['(e: PressEvent) => void'],
description:
'Handler that is called when the press is released over the target.',
},
onPressStart: {
type: 'enum',
values: ['(e: PressEvent) => void'],
description: 'Handler that is called when a press interaction starts.',
},
onPressEnd: {
type: 'enum',
values: ['(e: PressEvent) => void'],
description:
'Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target.',
},
onPressChange: {
type: 'enum',
values: ['(isPressed: boolean) => void'],
description: 'Handler that is called when the press state changes.',
},
onPressUp: {
type: 'enum',
values: ['(e: PressEvent) => void'],
description:
'Handler that is called when a press is released over the target, regardless of whether it started on the target or not.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const cellPropDefs: Record<string, PropDef> = {
id: {
type: 'enum',
values: ['Key'],
description: 'The unique id of the cell.',
},
textValue: {
type: 'string',
description:
"A string representation of the cell's contents, used for features like typeahead.",
},
leadingIcon: {
type: 'enum',
values: ['ReactNode'],
description: 'Optional icon to display before the cell content.',
},
...classNamePropDefs,
...stylePropDefs,
};
export const tablePaginationPropDefs: Record<string, PropDef> = {
offset: {
type: 'number',
description: 'The current offset (starting index) for pagination.',
},
pageSize: {
type: 'number',
description: 'The number of items per page.',
},
setOffset: {
type: 'enum',
values: ['(offset: number) => void'],
description: 'Handler that is called when the offset changes.',
},
setPageSize: {
type: 'enum',
values: ['(pageSize: number) => void'],
description: 'Handler that is called when the page size changes.',
},
rowCount: {
type: 'number',
description: 'The total number of rows in the table.',
},
onNextPage: {
type: 'enum',
values: ['() => void'],
description: 'Handler that is called when the next page is requested.',
},
onPreviousPage: {
type: 'enum',
values: ['() => void'],
description: 'Handler that is called when the previous page is requested.',
},
onPageSizeChange: {
type: 'enum',
values: ['(pageSize: number) => void'],
description: 'Handler that is called when the page size changes.',
},
showPageSizeOptions: {
type: 'boolean',
description: 'Whether to show the page size options.',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -0,0 +1,462 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
import { Chip } from '@/components/Chip';
// =============================================================================
// PropsTable Column Configuration (Table docs use description instead of responsive)
// =============================================================================
export const tablePropsColumns = [
{ key: 'prop' as const, width: '15%' },
{ key: 'type' as const, width: '25%' },
{ key: 'default' as const, width: '15%' },
{ key: 'description' as const, width: '45%' },
];
// For return values (no default column)
export const tableReturnColumns = [
{ key: 'prop' as const, width: '15%' },
{ key: 'type' as const, width: '25%' },
{ key: 'description' as const, width: '60%' },
];
// =============================================================================
// useTable Hook
// =============================================================================
export const useTableOptionsPropDefs: Record<string, PropDef> = {
mode: {
type: 'enum',
values: ['complete', 'offset', 'cursor'],
description: (
<>
Data fetching strategy (required). Use <Chip>complete</Chip> for
client-side data, <Chip>offset</Chip> for offset/limit APIs,{' '}
<Chip>cursor</Chip> for cursor-based pagination.
</>
),
},
getData: {
type: 'enum',
values: ['function'],
description:
'Function that returns or fetches data (required). Signature varies by mode.',
},
paginationOptions: {
type: 'enum',
values: ['object'],
description: (
<>
Pagination configuration including <Chip>pageSize</Chip>,{' '}
<Chip>pageSizeOptions</Chip>, and <Chip>initialOffset</Chip>.
</>
),
},
// Uncontrolled state
initialSort: {
type: 'enum',
values: ['SortDescriptor'],
description: 'Default sort configuration on first render (uncontrolled).',
},
initialSearch: {
type: 'string',
description: 'Default search value on first render (uncontrolled).',
},
initialFilter: {
type: 'enum',
values: ['TFilter'],
description: 'Default filter value on first render (uncontrolled).',
},
// Controlled state
sort: {
type: 'enum',
values: ['SortDescriptor'],
description: 'Current sort state (controlled).',
},
onSortChange: {
type: 'enum',
values: ['(sort: SortDescriptor) => void'],
description: 'Sort change handler (controlled).',
},
search: {
type: 'string',
description: 'Current search value (controlled).',
},
onSearchChange: {
type: 'enum',
values: ['(search: string) => void'],
description: 'Search change handler (controlled).',
},
filter: {
type: 'enum',
values: ['TFilter'],
description: 'Current filter value (controlled).',
},
onFilterChange: {
type: 'enum',
values: ['(filter: TFilter) => void'],
description: 'Filter change handler (controlled).',
},
// Client-side functions
sortFn: {
type: 'enum',
values: ['(data, sort) => data'],
description: (
<>
Client-side sort function. Only used with <Chip>complete</Chip> mode.
</>
),
},
searchFn: {
type: 'enum',
values: ['(data, query) => data'],
description: (
<>
Client-side search function. Only used with <Chip>complete</Chip> mode.
</>
),
},
filterFn: {
type: 'enum',
values: ['(data, filter) => data'],
description: (
<>
Client-side filter function. Only used with <Chip>complete</Chip> mode.
</>
),
},
};
export const useTableReturnPropDefs: Record<string, PropDef> = {
tableProps: {
type: 'enum',
values: ['object'],
description: (
<>
Props to spread onto the <Chip>Table</Chip> component. Includes data,
loading, error, pagination, and sort state.
</>
),
},
reload: {
type: 'enum',
values: ['() => void'],
description: 'Function to trigger a data refetch.',
},
search: {
type: 'enum',
values: ['{ value, onChange }'],
description: (
<>
Search state object for binding to a <Chip>SearchField</Chip> component.
</>
),
},
filter: {
type: 'enum',
values: ['{ value, onChange }'],
description: 'Filter state object for binding to filter controls.',
},
};
// =============================================================================
// Table Component
// =============================================================================
export const tablePropDefs: Record<string, PropDef> = {
columnConfig: {
type: 'enum',
values: ['ColumnConfig[]'],
description:
'Array of column configurations defining how each column renders.',
},
data: {
type: 'enum',
values: ['T[]'],
description: 'Array of data items to display in the table.',
},
loading: {
type: 'boolean',
default: 'false',
description: 'Whether the table is in a loading state.',
},
isStale: {
type: 'boolean',
default: 'false',
description:
'Whether the displayed data is stale while new data is loading.',
},
error: {
type: 'enum',
values: ['Error'],
description: 'Error object if data fetching failed.',
},
pagination: {
type: 'enum',
values: ['TablePaginationType'],
description: (
<>
Pagination configuration (required). Use{' '}
<Chip>{'{ type: "none" }'}</Chip> to disable or{' '}
<Chip>{'{ type: "page", ...props }'}</Chip> for pagination.
</>
),
},
sort: {
type: 'enum',
values: ['SortState'],
description: 'Sort state including current descriptor and change handler.',
},
rowConfig: {
type: 'enum',
values: ['RowConfig | RowRenderFn'],
description: (
<>
Row configuration object with <Chip>getHref</Chip>, <Chip>onClick</Chip>
, <Chip>getIsDisabled</Chip>, or a render function for custom rows.
</>
),
},
selection: {
type: 'enum',
values: ['TableSelection'],
description:
'Selection configuration including mode, behavior, selected keys, and change handler.',
},
emptyState: {
type: 'enum',
values: ['ReactNode'],
description: 'Content to display when the table has no data.',
},
...classNamePropDefs,
...stylePropDefs,
};
// =============================================================================
// ColumnConfig
// =============================================================================
export const columnConfigPropDefs: Record<string, PropDef> = {
id: {
type: 'string',
description: 'Unique identifier for the column.',
},
label: {
type: 'string',
description: 'Display label for the column header.',
},
cell: {
type: 'enum',
values: ['(item) => ReactNode'],
description: 'Render function for cell content.',
},
header: {
type: 'enum',
values: ['() => ReactNode'],
description: 'Optional custom render function for the header cell.',
},
isSortable: {
type: 'boolean',
default: 'false',
description: 'Whether the column supports sorting.',
},
isHidden: {
type: 'boolean',
default: 'false',
description: 'Whether the column is hidden.',
},
isRowHeader: {
type: 'boolean',
default: 'false',
description: 'Whether this column is the row header for accessibility.',
},
width: {
type: 'enum',
values: ['ColumnSize'],
description: 'Current width of the column.',
},
defaultWidth: {
type: 'enum',
values: ['ColumnSize'],
description: (
<>
Default width of the column (e.g., <Chip>1fr</Chip>, <Chip>200px</Chip>
).
</>
),
},
minWidth: {
type: 'enum',
values: ['ColumnStaticSize'],
description: 'Minimum width of the column.',
},
maxWidth: {
type: 'enum',
values: ['ColumnStaticSize'],
description: 'Maximum width of the column.',
},
};
// =============================================================================
// CellText
// Note: Extends React Aria Cell props
// =============================================================================
export const cellTextPropDefs: Record<string, PropDef> = {
title: {
type: 'string',
description: 'Primary text content of the cell.',
},
description: {
type: 'string',
description: 'Secondary description text displayed below the title.',
},
color: {
type: 'enum',
values: ['TextColors'],
description: 'Text color variant.',
},
leadingIcon: {
type: 'enum',
values: ['ReactNode'],
description: 'Icon displayed before the text content.',
},
href: {
type: 'string',
description: 'URL to navigate to when the cell is clicked.',
},
};
// =============================================================================
// CellProfile
// Note: Extends React Aria Cell props
// =============================================================================
export const cellProfilePropDefs: Record<string, PropDef> = {
name: {
type: 'string',
description: 'Name to display.',
},
src: {
type: 'string',
description: 'URL of the avatar image.',
},
description: {
type: 'string',
description: 'Secondary text displayed below the name.',
},
href: {
type: 'string',
description: 'URL to navigate to when clicked.',
},
color: {
type: 'enum',
values: ['TextColors'],
description: 'Text color variant.',
},
};
// =============================================================================
// TablePagination
// =============================================================================
export const tablePaginationPropDefs: Record<string, PropDef> = {
pageSize: {
type: 'number',
description: 'Number of items per page.',
},
pageSizeOptions: {
type: 'enum',
values: ['number[]'],
description: 'Available page size options for the dropdown.',
},
offset: {
type: 'number',
description: 'Current offset (starting index) in the data.',
},
totalCount: {
type: 'number',
description: 'Total number of items across all pages.',
},
hasNextPage: {
type: 'boolean',
description: 'Whether there is a next page available.',
},
hasPreviousPage: {
type: 'boolean',
description: 'Whether there is a previous page available.',
},
onNextPage: {
type: 'enum',
values: ['() => void'],
description: 'Handler called when navigating to the next page.',
},
onPreviousPage: {
type: 'enum',
values: ['() => void'],
description: 'Handler called when navigating to the previous page.',
},
onPageSizeChange: {
type: 'enum',
values: ['(size: number) => void'],
description: 'Handler called when the page size changes.',
},
showPageSizeOptions: {
type: 'boolean',
default: 'true',
description: 'Whether to show the page size dropdown.',
},
getLabel: {
type: 'enum',
values: ['(props) => string'],
description: 'Custom function to generate the pagination label text.',
},
};
// =============================================================================
// Primitives
// =============================================================================
export const tableRootPropDefs: Record<string, PropDef> = {
stale: {
type: 'boolean',
default: 'false',
description: (
<>
Whether the table data is stale (e.g., while fetching new data). Adds{' '}
<Chip>aria-busy</Chip> attribute.
</>
),
},
};
export const columnPropDefs: Record<string, PropDef> = {
isRowHeader: {
type: 'boolean',
description: 'Whether this column is a row header for accessibility.',
},
children: {
type: 'enum',
values: ['ReactNode'],
description: 'Column header content.',
},
};
export const rowPropDefs: Record<string, PropDef> = {
id: {
type: 'enum',
values: ['string | number'],
description: 'Unique identifier for the row.',
},
children: {
type: 'enum',
values: ['ReactNode | ((column) => ReactNode)'],
description:
'Row content. Can be a render function receiving column config.',
},
...classNamePropDefs,
...stylePropDefs,
};
+344 -93
View File
@@ -1,108 +1,359 @@
export const tableUsageSnippet = `import { Cell, CellText, ..., TableHeader, TablePagination } from '@backstage/ui';
// =============================================================================
// Usage
// =============================================================================
<Table>
<TableHeader>
<Column />
</TableHeader>
<TableBody>
<Row>
<CellText title="Example" />
<CellProfile />
</Row>
</TableBody>
</Table>
<TablePagination />`;
export const tableUsageSnippet = `import { Table, useTable, CellText, type ColumnConfig } from '@backstage/ui';
export const tableBasicSnippet = `import { Table, TableHeader, Column, TableBody, Row, CellText, CellProfile, TablePagination, useTable } from '@backstage/ui';
const data = [
{
name: 'The Beatles',
image: 'https://upload.wikimedia.org/wikipedia/en/thumb/4/42/Beatles_-...jpg',
genre: 'Rock, Pop, Psychedelic Rock',
yearFormed: 1960,
albums: 13
},
// ... more data
const columns: ColumnConfig<DataType>[] = [
{ id: 'name', label: 'Name', isRowHeader: true, cell: item => <CellText title={item.name} /> },
{ id: 'owner', label: 'Owner', cell: item => <CellText title={item.owner} /> },
];
// Uncontrolled pagination (easiest)
const { data: paginatedData, paginationProps } = useTable({
data,
pagination: {
defaultPageSize: 5,
function MyTable() {
const { tableProps } = useTable({
mode: 'complete',
getData: () => data,
});
return <Table columnConfig={columns} {...tableProps} />;
}`;
// =============================================================================
// Hero / Quick Start
// =============================================================================
export const tableHeroSnippet = `import { Table, CellText, useTable, type ColumnConfig } from '@backstage/ui';
const data = [
{ id: 1, name: 'Service A', owner: 'Team Alpha', type: 'service' },
{ id: 2, name: 'Service B', owner: 'Team Beta', type: 'website' },
{ id: 3, name: 'Library C', owner: 'Team Gamma', type: 'library' },
];
const columns: ColumnConfig<typeof data[0]>[] = [
{ id: 'name', label: 'Name', isRowHeader: true, cell: item => <CellText title={item.name} /> },
{ id: 'owner', label: 'Owner', cell: item => <CellText title={item.owner} /> },
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
];
function MyTable() {
const { tableProps } = useTable({
mode: 'complete',
getData: () => data,
});
return <Table columnConfig={columns} {...tableProps} />;
}`;
// =============================================================================
// Core Concepts
// =============================================================================
export const tableConceptsSnippet = `// What useTable returns
const {
tableProps, // Spread onto <Table />
reload, // Trigger data refetch
search, // { value, onChange } for search input
filter, // { value, onChange } for filters
} = useTable({ ... });`;
// =============================================================================
// Common Patterns
// =============================================================================
export const tableSortingSnippet = `const columns: ColumnConfig<Item>[] = [
{ id: 'name', label: 'Name', isSortable: true, cell: item => <CellText title={item.name} /> },
{ id: 'owner', label: 'Owner', isSortable: true, cell: item => <CellText title={item.owner} /> },
{ id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
];
const { tableProps } = useTable({
mode: 'complete',
getData: () => data,
initialSort: { column: 'name', direction: 'ascending' },
sortFn: (items, { column, direction }) => {
return [...items].sort((a, b) => {
const aVal = String(a[column]);
const bVal = String(b[column]);
const cmp = aVal.localeCompare(bVal);
return direction === 'descending' ? -cmp : cmp;
});
},
});
<Table>
<TableHeader>
<Column isRowHeader>Band name</Column>
<Column>Genre</Column>
<Column>Year formed</Column>
<Column>Albums</Column>
</TableHeader>
<TableBody>
{paginatedData?.map(item => (
<Row key={item.name}>
<CellProfile
name={item.name}
src={item.image}
href={item.website}
/>
<CellText title={item.genre} />
<CellText title={item.yearFormed.toString()} />
<CellText title={item.albums.toString()} />
</Row>
))}
</TableBody>
</Table>
<TablePagination {...paginationProps} />`;
return <Table columnConfig={columns} {...tableProps} />;`;
export const tableSelectionActionsSnippet = `import { Table, TableHeader, TableBody, Column, Row, CellText } from '@backstage/ui';
export const tablePaginationSnippet = `const { tableProps } = useTable({
mode: 'complete',
getData: () => data,
paginationOptions: {
pageSize: 10,
pageSizeOptions: [10, 25, 50],
},
});`;
function MyTable() {
const [selectedKeys, setSelectedKeys] = React.useState(new Set([]));
export const tableSearchSnippet = `const { tableProps, search } = useTable({
mode: 'complete',
getData: () => data,
searchFn: (items, query) => {
const lowerQuery = query.toLowerCase();
return items.filter(item =>
item.name.toLowerCase().includes(lowerQuery) ||
item.owner.toLowerCase().includes(lowerQuery)
);
},
});
return (
<>
<SearchField
aria-label="Search"
placeholder="Search..."
{...search}
/>
<Table columnConfig={columns} {...tableProps} />
</>
);`;
export const tableSelectionSnippet = `const [selected, setSelected] = useState<Set<string | number> | 'all'>(new Set());
const { tableProps } = useTable({
mode: 'complete',
getData: () => data,
});
return (
<Table
columnConfig={columns}
selection={{
mode: 'multiple',
behavior: 'toggle',
selected,
onSelectionChange: setSelected,
}}
{...tableProps}
/>
);`;
export const tableRowActionsHrefSnippet = `<Table
columnConfig={columns}
rowConfig={{
getHref: item => \`/catalog/\${item.namespace}/\${item.name}\`
}}
{...tableProps}
/>`;
export const tableRowActionsClickSnippet = `<Table
columnConfig={columns}
rowConfig={{
onClick: item => openDetailPanel(item)
}}
{...tableProps}
/>`;
export const tableRowActionsDisabledSnippet = `<Table
columnConfig={columns}
rowConfig={{
onClick: item => openDetailPanel(item),
getIsDisabled: item => item.status === 'archived',
}}
{...tableProps}
/>`;
export const tableEmptyStateSnippet = `const { tableProps, search } = useTable({
mode: 'complete',
getData: () => data,
searchFn: (items, query) => { /* ... */ },
});
return (
<Table
columnConfig={columns}
emptyState={
search.value
? <Text>No results match "{search.value}"</Text>
: <Text>No items yet. Create one to get started.</Text>
}
{...tableProps}
/>
);`;
// =============================================================================
// Server-Side Data
// =============================================================================
export const tableOffsetPaginationSnippet = `const { tableProps } = useTable({
mode: 'offset',
getData: async ({ offset, pageSize, sort, search, filter, signal }) => {
const response = await fetch(
\`/api/items?offset=\${offset}&limit=\${pageSize}&q=\${search}\`,
{ signal }
);
const { items, totalCount } = await response.json();
return {
data: items,
totalCount,
};
},
paginationOptions: {
pageSize: 20,
pageSizeOptions: [20, 50, 100],
},
});`;
export const tableCursorPaginationSnippet = `const { tableProps } = useTable({
mode: 'cursor',
getData: async ({ cursor, pageSize, sort, search, signal }) => {
const response = await fetch(
\`/api/items?cursor=\${cursor ?? ''}&limit=\${pageSize}&q=\${search}\`,
{ signal }
);
const { items, nextCursor, prevCursor, totalCount } = await response.json();
return {
data: items,
nextCursor,
prevCursor,
totalCount, // optional - enables "X of Y" display
};
},
paginationOptions: {
pageSize: 20,
pageSizeOptions: [20, 50, 100],
},
});`;
// =============================================================================
// Combining Features
// =============================================================================
export const tableCombinedSnippet = `interface TypeFilter {
type: string | null;
}
const columns: ColumnConfig<Item>[] = [
{ id: 'name', label: 'Name', isRowHeader: true, isSortable: true,
cell: item => <CellText title={item.name} description={item.description} /> },
{ id: 'owner', label: 'Owner', isSortable: true,
cell: item => <CellText title={item.owner} /> },
{ id: 'type', label: 'Type', isSortable: true,
cell: item => <CellText title={item.type} /> },
];
function ItemsTable() {
const [selected, setSelected] = useState<Set<string | number> | 'all'>(new Set());
const { tableProps, search, filter } = useTable<Item, TypeFilter>({
mode: 'offset',
initialSort: { column: 'name', direction: 'ascending' },
getData: async ({ offset, pageSize, sort, search, filter, signal }) => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(pageSize),
q: search,
...(sort && { sortBy: sort.column, sortDir: sort.direction }),
...(filter?.type && { type: filter.type }),
});
const response = await fetch(\`/api/items?\${params}\`, { signal });
const { items, totalCount } = await response.json();
return { data: items, totalCount };
},
});
return (
<Table
selectionMode="multiple"
selectionBehavior="toggle"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
onRowAction={(key) => console.log('Opening', key)}
>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Status</Column>
</TableHeader>
<TableBody>
<Row id="1">
<CellText title="Component A" />
<CellText title="Active" />
</Row>
<Row id="2">
<CellText title="Component B" />
<CellText title="Inactive" />
</Row>
</TableBody>
</Table>
<Flex direction="column" gap="4">
<Flex gap="4" align="end">
<SearchField
aria-label="Search"
label="Search"
placeholder="Search by name or description..."
value={search.value}
onChange={search.onChange}
/>
<Select
label="Type"
options={typeOptions}
value={filter.value?.type ?? ''}
onChange={value => filter.onChange({ type: value || null })}
/>
</Flex>
<Table
columnConfig={columns}
rowConfig={{
onClick: item => openDetailPanel(item),
}}
selection={{
mode: 'multiple',
behavior: 'toggle',
selected,
onSelectionChange: setSelected,
}}
emptyState={
search.value || filter.value?.type
? <Text>No results match your filters</Text>
: <Text>No items available</Text>
}
{...tableProps}
/>
</Flex>
);
}`;
export const tableSelectionModeSnippet = `<Table
selectionMode="multiple" // or "single"
selectionBehavior="toggle"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
>
{/* ... */}
</Table>`;
// =============================================================================
// Custom Tables
// =============================================================================
export const tableSelectionBehaviorSnippet = `<Table
selectionMode="multiple"
selectionBehavior="toggle" // or "replace"
selectedKeys={selectedKeys}
onSelectionChange={setSelectedKeys}
>
{/* ... */}
</Table>`;
export const tableCustomRowSnippet = `import { Fragment } from 'react';
const columns: ColumnConfig<Item>[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
header: () => <Text weight="bold">Name (required)</Text>,
cell: item => <CellText title={item.name} />,
},
{ id: 'owner', label: 'Owner', cell: item => <CellText title={item.owner} /> },
{ id: 'lifecycle', label: 'Lifecycle', cell: item => <CellText title={item.lifecycle} /> },
];
const { tableProps } = useTable({
mode: 'complete',
getData: () => data,
});
<Table
columnConfig={columns}
rowConfig={({ item }) => (
<Row
id={String(item.id)}
columns={columns}
className={item.lifecycle === 'experimental' ? styles.experimentalRow : undefined}
>
{column => (
<Fragment key={column.id}>{column.cell(item)}</Fragment>
)}
</Row>
)}
{...tableProps}
/>`;
export const tablePrimitivesSnippet = `<TableRoot>
<TableHeader>
<Column isRowHeader>Name</Column>
<Column>Owner</Column>
<Column>Type</Column>
</TableHeader>
<TableBody>
{items.map(item => (
<Row key={item.id} id={String(item.id)}>
<CellText title={item.name} />
<CellText title={item.owner} />
<CellText title={item.type} />
</Row>
))}
</TableBody>
</TableRoot>`;