Version 0.11.2

Table

A data table component with built-in pagination, sorting, and selection.

Loading table data.
Band name
Genre
Year formed
Albums

Usage#

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} />;
}

Core Concepts#

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 pagination
  • cursor - 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 / onSortChange
  • search / onSearchChange
  • filter / onFilterChange

Common Patterns#

Sorting#

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.

Loading table data.
Name
Owner
Type
Lifecycle

Pagination#

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.

Loading table data.
Name
Owner
Type
No data available

Row Selection#

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
Loading table data.
Name
Owner
Type

Selection mode:

Selection behavior:

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.

<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}
/>
Loading table data.
Name
Owner
Type

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.

Loading table data.
Name
Owner
Type
No items yet. Create one to get started.

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.

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.

Cursor Pagination#

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],
  },
});

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.

Loading...

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.

Loading table data.
Name
Owner
Lifecycle

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.

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

API Reference#

useTable#

The useTable hook manages data fetching, pagination, sorting, and filtering.

Options

PropTypeDefaultDescription
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

PropTypeDescription
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.

Table#

The main table component.

PropTypeDefaultDescription
columnConfig
ColumnConfig[]
-Array of column configurations defining how each column renders.
data
T[]
-Array of data items to display in the table.
loading
boolean
falseWhether the table is in a loading state.
isStale
boolean
falseWhether 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.

ColumnConfig#

PropTypeDefaultDescription
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
falseWhether the column supports sorting.
isHidden
boolean
falseWhether the column is hidden.
isRowHeader
boolean
falseWhether 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.

CellText#

PropTypeDefaultDescription
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.

CellProfile#

PropTypeDefaultDescription
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.

TablePagination#

PropTypeDefaultDescription
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
trueWhether to show the page size dropdown.
getLabel
(props) => string
-Custom function to generate the pagination label text.

Primitives#

Low-level components for building custom table layouts.

TableRoot

PropTypeDefaultDescription
stale
boolean
falseWhether the table data is stale (e.g., while fetching new data). Adds aria-busy attribute.

Inherits all React Aria Table props.

TableHeader

Inherits all React Aria TableHeader props.

TableBody

Inherits all React Aria TableBody props.

Column

PropTypeDefaultDescription
isRowHeader
boolean
-Whether this column is a row header for accessibility.
children
ReactNode
-Column header content.

Inherits all React Aria Column props.

Row

PropTypeDefaultDescription
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.

Cell

Inherits all React Aria Cell props.

Theming#

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-Table
  • bui-Table[data-stale="true"]
  • bui-Table[data-stale="false"]
  • bui-TableHeader
  • bui-TableBody
  • bui-TableRow
  • bui-TableHead
  • bui-TableHeadContent
  • bui-TableHeadSortButton
  • bui-TableCaption
  • bui-TableCell
  • bui-TableCellContentWrapper
  • bui-TableCellContent
  • bui-TableCellIcon
  • bui-TableCellProfileAvatar
  • bui-TableCellProfileAvatarImage
  • bui-TableCellProfileAvatarFallback
  • bui-TableCellProfileName
  • bui-TableCellProfileLink
  • bui-TableHeadSelection
  • bui-TableCellSelection

Changelog#

Version 0.11.0#

Breaking Changes

  • Breaking Redesigned Table component with new useTable hook API.

    • The Table component (React Aria wrapper) is renamed to TableRoot
    • New high-level Table component that handles data display, pagination, sorting, and selection
    • The useTable hook is completely redesigned with a new API supporting three pagination modes (complete, offset, cursor)
    • New types: ColumnConfig, TableProps, TableItem, UseTableOptions, UseTableResult

    New features include unified pagination modes, debounced query changes, stale data preservation during reloads, and row selection with toggle/replace behaviors. #32050

    Migration Guide:

    1. Update imports and use the new 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,
    +});
    
    1. Define columns and render with the new Table API:
    -<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} />
    

Changes

  • 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

Version 0.10.0#

Changes

  • Added row selection support with visual state styling for hover, selected, and pressed states. Fixed checkbox rendering to only show for multi-select toggle mode. #31907

Version 0.9.0#

Changes

  • Fixed Table Row component to properly support opening links in new tabs via right-click or Cmd+Click when using the href prop. #31680