A data table component with built-in pagination, sorting, and selection.
Band name | Genre | Year formed | Albums |
|---|
import { Table, useTable, CellText, type ColumnConfig } from '@backstage/ui';
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} /> },
];
function MyTable() {
const { tableProps } = useTable({
mode: 'complete',
data,
});
return <Table columnConfig={columns} {...tableProps} />;
}The Table component is designed around a hook + component pattern:
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.The hook supports three modes for different data scenarios:
complete - You have all data available upfront (client-side)offset - Your API uses offset/limit paginationcursor - Your API uses cursor-based pagination// What useTable returns
const {
tableProps, // Spread onto <Table />
reload, // Trigger data refetch
search, // { value, onChange } for search input
filter, // { value, onChange } for filters
} = useTable({ ... });Controlled vs uncontrolled state
By default, useTable manages sort, search, and filter state internally. Pass initialSort, initialSearch, or initialFilter to set starting values.
For full control over state, use the controlled props instead:
sort / onSortChangesearch / onSearchChangefilter / onFilterChangeColumns 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.
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.
Name | Owner | Type | Lifecycle |
|---|
Configure page size and available options through paginationOptions. The table displays navigation controls automatically.
const { tableProps } = useTable({
mode: 'complete',
data,
paginationOptions: {
pageSize: 10,
pageSizeOptions: [10, 25, 50],
},
});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.
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.
For server-side search with offset or cursor modes, the search query is passed to your getData function. See Server-Side Data.
Name | Owner | Type |
|---|---|---|
No data available | ||
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 timemultiple - Any number of rows can be selected, with a header checkbox for select-allSelection 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:
selected and onSelectionChange to fully manage state externallyonSelectionChange to let the Table manage state while receiving change notificationsName | Owner | Type |
|---|
Selection mode:
Selection behavior:
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.
<Table
columnConfig={columns}
rowConfig={{
getHref: item => `/catalog/${item.namespace}/${item.name}`
}}
{...tableProps}
/>Use onClick for custom actions like opening a panel or triggering a dialog.
<Table
columnConfig={columns}
rowConfig={{
onClick: item => openDetailPanel(item)
}}
{...tableProps}
/>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:
<Table
columnConfig={columns}
rowConfig={{
onClick: item => openDetailPanel(item),
getIsDisabled: item => item.status === 'archived',
}}
{...tableProps}
/>Name | Owner | Type |
|---|
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.
Name | Owner | Type |
|---|---|---|
No items yet. Create one to get started. | ||
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.
Use mode: 'offset' when your API accepts offset and limit (or skip and take) parameters.
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],
},
});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.
Use mode: 'cursor' when your API uses cursor-based pagination (common with GraphQL or APIs that return nextCursor/prevCursor tokens).
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],
},
});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.
Real-world tables often combine multiple features. Here's an example with search, sorting, pagination, and row selection working together.
For most use cases, useTable + Table provides everything you need. When you need more control, there are several ways you can customize your table.
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.
Name | Owner | Lifecycle |
|---|
When the Table component doesn't support your use case, you can compose with the lower-level primitives: TableRoot, TableHeader, TableBody, Column, and Row.
Name | Owner | Type |
|---|---|---|
authentication-and-authorization-service | security-team | service |
user-interface-dashboard-and-analytics-platform | frontend-team | website |
payment-gateway | finance-team | service |
real-time-analytics-processing-and-visualization-engine | data-team | service |
notification-center | platform-team | service |
The useTable hook manages data fetching, pagination, sorting, and filtering.
Options
| Prop | Type | Default | Description |
|---|---|---|---|
| mode | completeoffsetcursor | - | Data fetching strategy (required). Use complete for client-side data, offset for offset/limit APIs, cursor for cursor-based pagination. |
| getData | function | - | Function that returns or fetches data (required for "offset" and "cursor" modes). For the "complete" mode, either this or `data` must be provided. Signature varies by mode. |
| data | T[] | - | The data for the table. Only applicable for "complete" mode, and either this or `getData` must be provided. |
| paginationOptions | object | - | Pagination configuration including pageSize, pageSizeOptions, and initialOffset. |
| initialSort | SortDescriptor | - | Default sort configuration on first render (uncontrolled). |
| initialSearch | string | - | Default search value on first render (uncontrolled). |
| initialFilter | TFilter | - | Default filter value on first render (uncontrolled). |
| sort | SortDescriptor | - | Current sort state (controlled). |
| onSortChange | (sort: SortDescriptor) => void | - | Sort change handler (controlled). |
| search | string | - | Current search value (controlled). |
| onSearchChange | (search: string) => void | - | Search change handler (controlled). |
| filter | TFilter | - | Current filter value (controlled). |
| onFilterChange | (filter: TFilter) => void | - | Filter change handler (controlled). |
| sortFn | (data, sort) => data | - | Client-side sort function. Only used with complete mode. |
| searchFn | (data, query) => data | - | Client-side search function. Only used with complete mode. |
| filterFn | (data, filter) => data | - | Client-side filter function. Only used with complete mode. |
Return Value
| Prop | Type | Description |
|---|---|---|
| tableProps | object | Props to spread onto the Table component. Includes data, loading, error, pagination, and sort state. |
| reload | () => void | Function to trigger a data refetch. |
| search | { value, onChange } | Search state object for binding to a SearchField component. |
| filter | { value, onChange } | Filter state object for binding to filter controls. |
The main table component.
| Prop | Type | Default | Description |
|---|---|---|---|
| columnConfig | ColumnConfig[] | - | Array of column configurations defining how each column renders. |
| data | T[] | - | Array of data items to display in the table. |
| loading | boolean | false | Whether the table is in a loading state. |
| isStale | boolean | false | Whether the displayed data is stale while new data is loading. |
| error | Error | - | Error object if data fetching failed. |
| pagination | TablePaginationType | - | Pagination configuration (required). Use { type: "none" } to disable or { type: "page", ...props } for pagination. |
| sort | SortState | - | Sort state including current descriptor and change handler. |
| rowConfig | RowConfig | RowRenderFn | - | Row configuration object with getHref, onClick, getIsDisabled, or a render function for custom rows. |
| selection | TableSelection | - | Selection configuration including mode, behavior, selected keys, and change handler. |
| emptyState | ReactNode | - | Content to display when the table has no data. |
| className | string | - | Additional CSS class name for custom styling. |
| style | CSSProperties | - | Inline CSS styles object. |
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | - | Unique identifier for the column. |
| label | string | - | Display label for the column header. |
| cell | (item) => ReactNode | - | Render function for cell content. |
| header | () => ReactNode | - | Optional custom render function for the header cell. |
| isSortable | boolean | false | Whether the column supports sorting. |
| isHidden | boolean | false | Whether the column is hidden. |
| isRowHeader | boolean | false | Whether this column is the row header for accessibility. |
| width | ColumnSize | - | Current width of the column. |
| defaultWidth | ColumnSize | - | Default width of the column (e.g., 1fr, 200px). |
| minWidth | ColumnStaticSize | - | Minimum width of the column. |
| maxWidth | ColumnStaticSize | - | Maximum width of the column. |
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | - | Primary text content of the cell. |
| description | string | - | Secondary description text displayed below the title. |
| color | TextColors | - | Text color variant. |
| leadingIcon | ReactNode | - | Icon displayed before the text content. |
| href | string | - | URL to navigate to when the cell is clicked. |
Inherits all React Aria Cell props.
| Prop | Type | Default | Description |
|---|---|---|---|
| name | string | - | Name to display. |
| src | string | - | URL of the avatar image. |
| description | string | - | Secondary text displayed below the name. |
| href | string | - | URL to navigate to when clicked. |
| color | TextColors | - | Text color variant. |
Inherits all React Aria Cell props.
| Prop | Type | Default | Description |
|---|---|---|---|
| pageSize | number | - | Number of items per page. |
| pageSizeOptions | number[] | - | Available page size options for the dropdown. |
| offset | number | - | Current offset (starting index) in the data. |
| totalCount | number | - | Total number of items across all pages. |
| hasNextPage | boolean | - | Whether there is a next page available. |
| hasPreviousPage | boolean | - | Whether there is a previous page available. |
| onNextPage | () => void | - | Handler called when navigating to the next page. |
| onPreviousPage | () => void | - | Handler called when navigating to the previous page. |
| onPageSizeChange | (size: number) => void | - | Handler called when the page size changes. |
| showPageSizeOptions | boolean | true | Whether to show the page size dropdown. |
| getLabel | (props) => string | - | Custom function to generate the pagination label text. |
Low-level components for building custom table layouts.
| Prop | Type | Default | Description |
|---|---|---|---|
| stale | boolean | false | Whether the table data is stale (e.g., while fetching new data). Adds aria-busy attribute. |
Inherits all React Aria Table props.
Inherits all React Aria TableHeader props.
Inherits all React Aria TableBody props.
| Prop | Type | Default | Description |
|---|---|---|---|
| isRowHeader | boolean | - | Whether this column is a row header for accessibility. |
| children | ReactNode | - | Column header content. |
Inherits all React Aria Column props.
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | number | - | Unique identifier for the row. |
| children | ReactNode | ((column) => ReactNode) | - | Row content. Can be a render function receiving column config. |
| className | string | - | Additional CSS class name for custom styling. |
| style | CSSProperties | - | Inline CSS styles object. |
Inherits all React Aria Row props.
Inherits all React Aria Cell props.
Our theming system is based on a mix between CSS classes, CSS variables and data attributes. If you want to customise this component, you can use one of these class names below.
bui-Tablebui-Table[data-stale="true"]bui-Table[data-stale="false"]bui-TableHeaderbui-TableBodybui-TableRowbui-TableHeadbui-TableHeadContentbui-TableHeadSortButtonbui-TableCaptionbui-TableCellbui-TableCellContentWrapperbui-TableCellContentbui-TableCellIconbui-TableCellProfileAvatarbui-TableCellProfileAvatarImagebui-TableCellProfileAvatarFallbackbui-TableCellProfileNamebui-TableCellProfileLinkbui-TableHeadSelectionbui-TableCellSelectionBreaking Redesigned Table component with new useTable hook API.
Table component (React Aria wrapper) is renamed to TableRootTable component that handles data display, pagination, sorting, and selectionuseTable hook is completely redesigned with a new API supporting three pagination modes (complete, offset, cursor)ColumnConfig, TableProps, TableItem, UseTableOptions, UseTableResultNew features include unified pagination modes, debounced query changes, stale data preservation during reloads, and row selection with toggle/replace behaviors. #32050
Migration Guide:
useTable hook:-import { Table, useTable } from '@backstage/ui';
-const { data, paginationProps } = useTable({ data: items, pagination: {...} });
+import { Table, useTable, type ColumnConfig } from '@backstage/ui';
+const { tableProps } = useTable({
+ mode: 'complete',
+ getData: () => items,
+});
-<Table aria-label="My table">
- <TableHeader>...</TableHeader>
- <TableBody items={data}>...</TableBody>
-</Table>
-<TablePagination {...paginationProps} />
+const columns: ColumnConfig<Item>[] = [
+ { id: 'name', label: 'Name', isRowHeader: true, cell: item => <CellText title={item.name} /> },
+ { id: 'type', label: 'Type', cell: item => <CellText title={item.type} /> },
+];
+
+<Table columnConfig={columns} {...tableProps} />
Fixed Table sorting indicator not being visible when a column is actively sorted. #32350
Added support for column width configuration in Table component. Columns now accept width, defaultWidth, minWidth, and maxWidth props for responsive layout control. #32336
Added support for custom pagination options in useTable hook and Table component. You can now configure pageSizeOptions to customize the page size dropdown, and hook into pagination events via onPageSizeChange, onNextPage, and onPreviousPage callbacks. When pageSize doesn't match any option, the first option is used and a warning is logged. #32321
Fixed missing border styles on table selection cells in multi-select mode. #32369
Added className and style props to the Table component. #32342
href prop. #31680