Skip to content

Data Table

The list screen every admin, ERP and SaaS app has: a table of rows with search, filters, sorting, pages, selection with bulk actions, a row menu with confirmed actions, CSV export, loading, empty and error states, and cards on a narrow screen. It never fetches: you pass the rows (or one page of them with serverSide) and handle the actions. Permissions only change what people see and can try; they are not security, so the server must check every action again.

Blockv0.1.0experimentalWCAG 2.2 AAView spec

A ready-made piece, built from the library's own parts.

A block puts several components together into something you would otherwise assemble by hand. It never fetches data: you give it the data, or answer its callbacks. It is copied into your project like any component, with the parts it uses, so you can change anything.

Basic

Full screen (opens in a new tab)

24 results

OrderCustomerStatusRegionTotalPlaced
#1042Northgate FoodsPaidNorth$240Sep 3, 2026
#1041Harbor MillsPendingWest$617Aug 10, 2026
#1040Brightwater SuppliesPaidEast$994Jul 17, 2026
#1039Lindqvist TextilesOverdueSouth$1,371Jun 24, 2026
#1038Orchard Row CafeRefundedNorth$1,748May 7, 2026
#1037Kestrel LogisticsPaidWest$2,125Apr 14, 2026
#1036Maplewood DentalPaidEast$2,502Sep 21, 2026
#1035Ironbridge ToolsPendingSouth$479Aug 4, 2026

Showing 1 to 8 of 24

Searchable and filterable

Full screen (opens in a new tab)
Status
Region

24 results

OrderCustomerStatusRegionTotalPlaced
#1036Maplewood DentalPaidEast$2,502Sep 21, 2026
#1030Northgate FoodsPaidNorth$2,364Sep 15, 2026
#1024Maplewood DentalPaidEast$2,226Sep 9, 2026
#1042Northgate FoodsPaidNorth$240Sep 3, 2026
#1029Harbor MillsPendingWest$341Aug 22, 2026
#1023Ironbridge ToolsPendingSouth$2,603Aug 16, 2026
#1041Harbor MillsPendingWest$617Aug 10, 2026
#1035Ironbridge ToolsPendingSouth$479Aug 4, 2026

Showing 1 to 8 of 24

With selection and bulk actions

Full screen (opens in a new tab)

24 results

OrderCustomerStatusRegionTotalPlaced
#1042Northgate FoodsPaidNorth$240Sep 3, 2026
#1041Harbor MillsPendingWest$617Aug 10, 2026
#1040Brightwater SuppliesPaidEast$994Jul 17, 2026
#1039Lindqvist TextilesOverdueSouth$1,371Jun 24, 2026
#1038Orchard Row CafeRefundedNorth$1,748May 7, 2026
#1037Kestrel LogisticsPaidWest$2,125Apr 14, 2026
#1036Maplewood DentalPaidEast$2,502Sep 21, 2026
#1035Ironbridge ToolsPendingSouth$479Aug 4, 2026

Showing 1 to 8 of 24

Row actions and delete

Full screen (opens in a new tab)

6 results

OrderCustomerStatusRegionTotalPlacedActions
Northgate FoodsPaidNorth$240Sep 3, 2026
Harbor MillsPendingWest$617Aug 10, 2026
Brightwater SuppliesPaidEast$994Jul 17, 2026
Lindqvist TextilesOverdueSouth$1,371Jun 24, 2026
Orchard Row CafeRefundedNorth$1,748May 7, 2026
Kestrel LogisticsPaidWest$2,125Apr 14, 2026

Showing 1 to 6 of 6

States

Full screen (opens in a new tab)

24 results

OrderCustomerStatusRegionTotalPlaced
#1042Northgate FoodsPaidNorth$240Sep 3, 2026
#1041Harbor MillsPendingWest$617Aug 10, 2026
#1040Brightwater SuppliesPaidEast$994Jul 17, 2026
#1039Lindqvist TextilesOverdueSouth$1,371Jun 24, 2026
#1038Orchard Row CafeRefundedNorth$1,748May 7, 2026
#1037Kestrel LogisticsPaidWest$2,125Apr 14, 2026

Showing 1 to 6 of 24

Server side

Full screen (opens in a new tab)
Status

Loading orders

OrderCustomerStatusRegionTotalPlaced
Loading row
Loading row
Loading row
Loading row
Loading row

Permissions

Full screen (opens in a new tab)

24 results

Only finance can export orders
OrderCustomerStatusRegionTotalPlacedActions
#1042Northgate FoodsPaidNorth$240Sep 3, 2026
#1041Harbor MillsPendingWest$617Aug 10, 2026
#1040Brightwater SuppliesPaidEast$994Jul 17, 2026
#1039Lindqvist TextilesOverdueSouth$1,371Jun 24, 2026
#1038Orchard Row CafeRefundedNorth$1,748May 7, 2026
#1037Kestrel LogisticsPaidWest$2,125Apr 14, 2026

Showing 1 to 6 of 24

Url state

Full screen (opens in a new tab)
Status
Region

24 results

OrderCustomerStatusRegionTotalPlaced
#1042Northgate FoodsPaidNorth$240Sep 3, 2026
#1041Harbor MillsPendingWest$617Aug 10, 2026
#1040Brightwater SuppliesPaidEast$994Jul 17, 2026
#1039Lindqvist TextilesOverdueSouth$1,371Jun 24, 2026
#1038Orchard Row CafeRefundedNorth$1,748May 7, 2026
#1037Kestrel LogisticsPaidWest$2,125Apr 14, 2026
#1036Maplewood DentalPaidEast$2,502Sep 21, 2026
#1035Ironbridge ToolsPendingSouth$479Aug 4, 2026

Showing 1 to 8 of 24

ClassNames

Full screen (opens in a new tab)
Status

24 results

OrderCustomerStatusRegionTotalPlaced
#1042Northgate FoodsPaidNorth$240Sep 3, 2026
#1041Harbor MillsPendingWest$617Aug 10, 2026
#1040Brightwater SuppliesPaidEast$994Jul 17, 2026
#1039Lindqvist TextilesOverdueSouth$1,371Jun 24, 2026
#1038Orchard Row CafeRefundedNorth$1,748May 7, 2026
#1037Kestrel LogisticsPaidWest$2,125Apr 14, 2026

Showing 1 to 6 of 24

User management

Full screen (opens in a new tab)
View as

Users

Everyone with access to this workspace.
Role
Status

12 results

NameEmailRoleStatusLast activeActions
amara.okafor@example.comVieweractiveOct 1, 2026
lena.fischer@example.comEditoractiveOct 6, 2026
tomas.novak@example.comAdmininvitedOct 11, 2026
priya.raman@example.comVieweractiveOct 16, 2026
mateo.silva@example.comEditorsuspendedOct 21, 2026
hana.sato@example.comAdminactiveOct 26, 2026
jonas.berg@example.comVieweractiveOct 3, 2026
imani.brooks@example.comEditorinvitedOct 8, 2026

Showing 1 to 8 of 12

Installation

npx rdloom add data-table

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/data-table.json

Works, but without upgrade tracking.

Usage

import { Badge, DataTable, type DataTableColumn } from "@rdloom/react";

<Frame>
  <DataTable label="Orders" rows={orders} columns={columns} getRowId={(o) => o.id} pageSize={8} />
</Frame>

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
rowsrequired

The rows to show. With serverSide, only the current page.

readonly any[]none
columnsrequired

The columns: { id, header, cell?(row), value?(row), sortable?, align?, hideOnMobile?, width? }. The first column names each row (and is the title of a phone card). value is what sorting, searching and the CSV read; without it the row's field named id is used.

DataTableColumn<any>[]none
getRowIdrequired

A stable, unique id for a row, such as its database id. Used for selection, focus after a delete and the open link.

(row: any) => stringnone
label

Accessible name of the table and the section, e.g. "Orders".

string"Table"
state

The data state, when your data layer gives you one: loading, empty, error or ready. It wins over isLoading and error. A ready table with no matching rows says nothing matches.

DataStatenone
isLoading

Show loading rows and skeleton cards, disable the controls, and mark the section busy.

booleanfalse
error

Something went wrong loading: a message to show instead of the rows (true uses a general one).

string | booleannone
onRetry

Adds a Try again button to the error state.

() => voidnone
emptyTitle

Title shown when there are no rows at all. Defaults to "Nothing here yet".

stringnone
emptyDescription

Text under the empty title.

stringnone
emptyAction

A control shown in the empty state when there are no rows at all (not when a search found nothing), such as an Add button.

ReactNodenone
searchable

Add a search box. true looks in every column; or list the column ids to look in.

boolean | string[]none
searchPlaceholder

Placeholder of the search box.

string"Search"
filters

Filters shown as selects in the toolbar and in a sheet on a narrow screen: { id, label, options, match? }. Options are text or { value, label }.

DataTableFilter<any>[]none
pageSize

Rows per page.

number10
query

The search, filters, sort and page, when you keep them yourself (for example in the address). Without it the table keeps them.

DataTableQuerynone
defaultQuery

Start with a search, filters, a sort or a page already set.

Partial<DataTableQuery>none
onQueryChange

Called with the new query whenever the search, a filter, the sort or the page changes (typing waits for a short pause with serverSide). With serverSide, answer it with new rows.

(query: DataTableQuery) => voidnone
onSearch

Called with the search text whenever it changes.

(search: string) => voidnone
onFilter

Called with the chosen values of every filter whenever a filter changes or the filters are cleared.

(filters: Record<string, string[]>) => voidnone
serverSide

The rows live on a server. The table then shows exactly the rows you pass and asks for new ones through onQueryChange.

booleanfalse
totalCount

With serverSide: how many rows match in all, so the pages and the count are right.

numbernone
selectionMode

Add checkboxes to pick rows. The header box selects the rows on the page.

"none" | "multiple""none"
selectedKeys

Controlled selection: the ids of the selected rows.

Set<string>none
defaultSelectedKeys

Rows selected at the start.

Iterable<string>none
onSelectionChange

Called with the ids of the selected rows.

(keys: Set<string>) => voidnone
bulkActions

Actions for the selected rows, shown in a bar while something is selected: { id, label, icon?, variant?, permission?, confirm?, onAction(rows) }. A confirm opens a dialog that also says how many rows are affected. The selection clears when the action succeeds.

DataTableBulkAction<any>[]none
rowActions

The actions of one row, shown in a More menu at the end of the row and of the card: { id, label, permission?, confirm?, variant?, onAction }. Return an empty list for none. After a delete, focus moves to the next row.

(row: any) => DataTableRowAction<any>[]none
onRowOpen

Makes the first cell of each row a button and calls this with the row, e.g. to open a details page or panel. onSelect is called too.

(row: any) => voidnone
onSelect

The conventional name for opening a row: called together with onRowOpen.

(row: any) => voidnone
rowHref

Makes the first cell of each row a link to this address instead of a button (onRowOpen and onSelect are still called).

(row: any) => stringnone
onExport

Replaces the built-in CSV download. It gets the rows that match (all pages when the list is in memory) and the query. With serverSide the Export button is shown only when you give this.

(rows: any[], query: DataTableQuery) => voidnone
permissions

What the app allows, by action: export (Export CSV), select (the checkboxes), bulk (the bulk bar as a whole) and rowActions (the More menu). Each answer is "allow", "disabled", "hidden", true, false or { state, reason }. Hidden removes the control; disabled keeps it focusable with the reason where there is a button, and leaves selection off. Single actions have a permission of their own. This only changes what people see: the server must check again.

Permissions<"export" | "select" | "bulk" | "rowActions">none
classNames

Class names for the parts of the block, by slot name, added after the built-in ones so you can restyle one part without editing the file. Slots: root, toolbar, search, filters, count, export, bulk-bar, table, header, row, cell, cards, card, empty, error, footer, pagination.

Partial<Record<"root" | "toolbar" | "search" | "filters" | "count" | "export" | "bulk-bar" | "table" | "header" | "row" | "cell" | "cards" | "card" | "empty" | "error" | "footer" | "pagination", string>>none
density

Row spacing of the table.

"compact" | "standard" | "comfortable""standard"
layout

Table or cards. auto shows cards when the block itself is narrower than about 640px (not just the screen).

"auto" | "table" | "cards""auto"

Accessibility

Role region, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab: moves through the search, the filters, Export, the table (one stop), the open buttons, the More buttons and the page buttons
  • In the table: arrow keys move between cells; Enter or Space on a column header sorts; Space on a row checkbox selects
  • In the More menu: Enter or Space opens it, arrow keys choose, Enter runs, Escape closes
  • In a filter: Enter or Space opens the list, arrow keys choose, Escape closes
  • Escape in the search box clears it

Screen readers announce

  • "Orders, region" then "Search orders, search edit text"
  • After typing: "3 results" (polite)
  • The table reads as "Orders, table, 10 rows, 6 columns"; a cell reads its row header, column and value
  • A sort reads "Total, column header, sorted descending"
  • Selecting rows reads "3 selected"

What your code must do

  • A labelled region around everything; the table and the phone cards have their own names
  • A real table with a row header (the first column) and sortable column headers that expose their sort direction
  • The count of results is a polite status message, so filtering is announced; so is the number selected ("3 selected"), and the result of an action
  • The bulk bar appears without taking focus; after a delete focus moves to the next row, or to the table, never to the page top
  • Search, filters and the More button each have a name (the More button names the row)
  • Loading marks the region busy and shows skeletons; the controls are disabled while loading
  • An empty list says why (nothing yet, or nothing matches) and offers Clear filters when a search or filter is set; an error says what happened and offers a retry
  • A disabled action stays focusable and says why, instead of vanishing without a word
  • Below about 640px the table is replaced by cards (a list); only one of the two is in the accessibility tree at a time
  • CSV export neutralises cells that start with = + - or @ so a spreadsheet can't run them as formulas

Block contract

Data
A list of rows of any shape, described by columns, or one page of them when serverSide is on.
Data states
loading, empty, error, ready
Permissions
export, select, bulk, rowActions, per bulk action and per row action (permission field)
Events
onQueryChange, onSearch, onFilter, onSelect, onRowOpen, onSelectionChange, onExport, onRetry, bulkActions[].onAction, rowActions[].onAction
You can replace
columns (cell, value, align, width); filters (options, match); bulkActions and rowActions; density; layout; emptyAction; the parts it is built from (Table, Pagination, Select, Menu, ActionButton, AlertDialog)

Guidelines

Use it when

  • Any admin, ERP or SaaS list: users, orders, invoices, products
  • You want search, filters, sorting, paging, selection, bulk and row actions and export without wiring each one

Avoid it when

  • Tens of thousands of rows you must scroll or edit like a spreadsheet: use DataGrid
  • A short list with no search or paging: use Table

Don't

  • Loading every row into the browser when there are many thousands: use serverSide
  • A bulk action that deletes without a confirm
  • UI permission is not security: hiding or disabling an action only changes what people see, so the server must check access again for every request
  • Keeping the selection after rows were deleted elsewhere: let the selection clear when the action succeeds

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-surface-default
  • --rd-color-surface-subtle
  • --rd-color-border-default
  • --rd-color-border-strong
  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-focus-ring
  • --rd-radius-control
  • --rd-radius-overlay
  • --rd-elevation-raised
  • --rd-size-control-md