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 / onFilterChangeEvery cell rendered via ColumnConfig.cell (or inside a custom RowRenderFn) must return a cell component as the top-level element. The available cell components are:
CellText — displays a title with optional description and icon.CellProfile — displays an avatar with a name and optional description.Cell — a generic wrapper for fully custom cell content.Returning bare text, React fragments, or other elements without wrapping them in one of these cell components will break the table layout.
// ✅ Correct — CellText is the top-level element
cell: item => <CellText title={item.name} />;
// ✅ Correct — Cell wraps custom content
cell: item => (
<Cell>
<MyCustomContent value={item.name} />
</Cell>
);
// ❌ Wrong — bare text without a cell wrapper
cell: item => <span>{item.name}</span>;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.
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. In complete mode, set type: 'none' to disable pagination and show all rows.
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 |
|---|---|---|
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 |
|---|---|---|
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.
Name | Owner | Type |
|---|---|---|
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 | - | Pagination configuration. | |
| 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. |
| showPaginationLabel | boolean | true | Whether to display the pagination label (e.g., "1 - 20 of 150"). When false, only navigation controls are shown. |
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. |
| loading | boolean | false | Whether the table is in a loading state (e.g., initial data fetch). Adds aria-busy attribute and data-loading data attribute for styling. |
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. |
| noTrack | boolean | - | Suppresses the automatic analytics click event, e.g. if you already have custom tracking. |
| 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-TableTable Breaking Centralized client-side routing in BUIProvider. Components like Link, ButtonLink, Tabs, Menu, TagGroup, and Table now require a BUIProvider rendered inside a React Router context for client-side navigation to work. #33267
Migration Guide:
This change requires updating @backstage/plugin-app and @backstage/core-app-api alongside @backstage/ui. If you only upgrade @backstage/ui, BUI components will fall back to full-page navigation.
If you cannot upgrade all packages together, or if you have a custom app shell, add a BUIProvider inside your Router:
+ import { BUIProvider } from '@backstage/ui';
<BrowserRouter>
+ <BUIProvider>
<AppContent />
+ </BUIProvider>
</BrowserRouter>
Row Added analytics capabilities to the component library. Components with navigation behavior (Link, ButtonLink, Tab, MenuItem, Tag, Row) now fire analytics events on click when a BUIProvider is present.
New exports: BUIProvider, useAnalytics, getNodeText, and associated types (AnalyticsTracker, UseAnalyticsFn, BUIProviderProps, AnalyticsEventAttributes).
Components with analytics support now accept a noTrack prop to suppress event firing. #33150
Table Root Added a loading prop and data-loading data attribute to TableRoot, allowing consumers to distinguish between stale data and initial loading states. Both stale and loading set aria-busy on the table. #33322
Table Improved the Table component loading state to show a skeleton UI with visible headers instead of plain "Loading..." text. The table now renders its full structure during loading, with animated skeleton rows in place of data. The loading state includes proper accessibility support with aria-busy on the table and screen reader announcements. #33322
Table Table Pagination Migrated all components from useStyles to useDefinition hook. Exported OwnProps types for each component, enabling better type composition for consumers. #33050
Table Fixed Table column headers overflowing and wrapping when there is not enough space. Headers now truncate with ellipsis instead. #33256
Row Fixed Table row hover, selected, pressed, and disabled background states to use the correct neutral token level based on the container background. #33394
Row Fixed Table rows showing a pointer cursor when not interactive. Rows now only show cursor: pointer when they have an href, are selectable, or are pressable. #33256
Row Fixed Table rows with external href values to open in a new tab by automatically applying target="_blank" and rel="noopener noreferrer". #33353
Table Updated Table selection checkboxes to use aria-label instead of empty fragment children, improving accessibility and removing the unnecessary label gap in the selection cells. #33394
Table Allow data to be passed directly to the useTable hook using the property data instead of getData() for mode "complete".
This simplifies usage as data changes, rather than having to perform a useEffect when data changes, and then reloading the data. It also happens immediately, so stale data won't remain until a rerender (with an internal async state change), so less flickering. #32685
Table Fixed changing columns after first render from crashing. It now renders the table with the new column layout as columns change. #32684
Row Fixed components to not require a Router context when rendering without internal links. #32373
Table The Table component now wraps the react-aria-components Table with a ResizableTableContainer only if any column has a width property set. This means that column widths can adapt to the content otherwise (if no width is explicitly set). #32686
Table Breaking 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} />
Table Fixed Table sorting indicator not being visible when a column is actively sorted. #32350
Table Added support for column width configuration in Table component. Columns now accept width, defaultWidth, minWidth, and maxWidth props for responsive layout control. #32336
Table 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
Table Fixed missing border styles on table selection cells in multi-select mode. #32369
Table Added className and style props to the Table component. #32342
href prop. #31680