Skip to content

Customer Table

A ready-to-use customer list: search, filters, sorting, pages, CSV export, loading rows, an empty state, an error with retry, cards on a phone, and totals with a chart above that follow the filters. Give it customers and it works.

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)
Customers: 24.
Active: 14. 58% of customers
Monthly revenue: $3,234.
Churned: 3.
Monthly revenue by planMonthly revenue split across 4 plans, $3,234 in all.
Status
Plan
Sort by

24 customers

  • Ada Lovelaceada@example.com · Analytical Coactive
    Plan
    Free
    Revenue
    $0
    Joined
    Jan 3, 2026
  • Grace Hoppergrace@example.com · Compiler Labsactive
    Plan
    Pro
    Revenue
    $29
    Joined
    Feb 8, 2026
  • Katherine Johnsonkatherine@example.com · Orbit Systemstrial
    Plan
    Team
    Revenue
    $99
    Joined
    Mar 13, 2026
  • Alan Turingalan@example.com · Enigma Worksoverdue
    Plan
    Enterprise
    Revenue
    $499
    Joined
    Apr 18, 2026
  • Margaret Hamiltonmargaret@example.com · Apollo Softwareactive
    Plan
    Free
    Revenue
    $0
    Joined
    May 23, 2026
  • Dennis Ritchiedennis@example.com · Bell Studiochurned
    Plan
    Pro
    Revenue
    $29
    Joined
    Jun 4, 2026
  • Barbara Liskovbarbara@example.com · Substitution Incactive
    Plan
    Team
    Revenue
    $99
    Joined
    Jul 9, 2026
  • Edsger Dijkstraedsger@example.com · Shortest Pathactive
    Plan
    Enterprise
    Revenue
    $499
    Joined
    Aug 14, 2026

Showing 1 to 8 of 24

Server side

Full screen (opens in a new tab)
Status
Plan
Sort by

Loading customers

Loading and empty

Full screen (opens in a new tab)
Status
Plan
Sort by

Loading customers

Custom insights

Full screen (opens in a new tab)
Monthly revenue: $48,200. Up 12% compared with last month.
New customers: 128. Down 4% compared with last month.
Churn: 1.8%. Down 0.4% compared with last month, an improvement.
Trials: 36. 9 end this week
Revenue, last 7 monthsArea chart of Revenue across 7 points. Revenue is highest at $48,200 (Oct) and lowest at $31,000 (Apr).
Status
Plan
Sort by

4 customers

  • Ada Lovelaceada@example.comactive
    Plan
    Pro
    Revenue
    $99
    Joined
    Jan 14, 2026
  • Grace Hoppergrace@example.comactive
    Plan
    Team
    Revenue
    $499
    Joined
    Feb 2, 2026
  • Alan Turingalan@example.comtrial
    Plan
    Pro
    Revenue
    $0
    Joined
    Mar 9, 2026
  • Margaret Hamiltonmargaret@example.comoverdue
    Plan
    Team
    Revenue
    $499
    Joined
    Mar 21, 2026

Showing 1 to 4 of 4

Without insights

Full screen (opens in a new tab)
Status
Plan
Sort by

5 customers

  • Ada Lovelaceada@example.com · Analytical Coactive
    Plan
    Pro
    Revenue
    €99
    Joined
    14 Jan 2026
  • Grace Hoppergrace@example.com · Compiler Labsactive
    Plan
    Team
    Revenue
    €499
    Joined
    2 Feb 2026
  • Alan Turingalan@example.comtrial
    Plan
    Free
    Revenue
    €0
    Joined
    9 Mar 2026
  • Margaret Hamiltonmargaret@example.comoverdue
    Plan
    Team
    Revenue
    €499
    Joined
    21 Mar 2026
  • Dennis Ritchiedennis@example.comchurned
    Plan
    Pro
    Revenue
    €99
    Joined
    30 Nov 2025

Showing 1 to 5 of 5

Row action

Full screen (opens in a new tab)
Status
Plan
Sort by

3 customers

  • ada@example.com · Analytical Coactive
    Plan
    Pro
    Revenue
    $99
    Joined
    Jan 14, 2026
  • grace@example.com · Compiler Labsactive
    Plan
    Team
    Revenue
    $499
    Joined
    Feb 2, 2026
  • alan@example.comtrial
    Plan
    Free
    Revenue
    $0
    Joined
    Mar 9, 2026

Showing 1 to 3 of 3

Url state

Full screen (opens in a new tab)

/customers

Status
Plan
Sort by

12 customers

  • Ada Lovelaceada@example.comactive
    Plan
    Free
    Revenue
    $0
    Joined
    Jan 3, 2026
  • Grace Hoppergrace@example.comactive
    Plan
    Pro
    Revenue
    $29
    Joined
    Feb 8, 2026
  • Katherine Johnsonkatherine@example.comtrial
    Plan
    Team
    Revenue
    $99
    Joined
    Mar 13, 2026
  • Alan Turingalan@example.comoverdue
    Plan
    Enterprise
    Revenue
    $499
    Joined
    Apr 18, 2026
  • Margaret Hamiltonmargaret@example.comactive
    Plan
    Free
    Revenue
    $0
    Joined
    May 23, 2026

Showing 1 to 5 of 12

Permissions

Full screen (opens in a new tab)
Status
Plan
Sort by

3 customers

Only admins can export customers
  • ada@example.com · Analytical Coactive
    Plan
    Pro
    Revenue
    $99
    Joined
    Jan 14, 2026
  • grace@example.com · Compiler Labsactive
    Plan
    Team
    Revenue
    $499
    Joined
    Feb 2, 2026
  • alan@example.comtrial
    Plan
    Free
    Revenue
    $0
    Joined
    Mar 9, 2026

Showing 1 to 3 of 3

ClassNames

Full screen (opens in a new tab)
Status
Plan
Sort by

3 customers

  • Ada Lovelaceada@example.com · Analytical Coactive
    Plan
    Pro
    Revenue
    $99
    Joined
    Jan 14, 2026
  • Grace Hoppergrace@example.com · Compiler Labsactive
    Plan
    Team
    Revenue
    $499
    Joined
    Feb 2, 2026
  • Alan Turingalan@example.comtrial
    Plan
    Free
    Revenue
    $0
    Joined
    Mar 9, 2026

Showing 1 to 3 of 3

Installation

npx rdloom add customer-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/customer-table.json

Works, but without upgrade tracking.

Usage

import { CustomerTable, type Customer } from "@rdloom/react";

<div className="mx-auto w-[60rem] max-w-full">
  <CustomerTable customers={customers} pageSize={8} />
</div>

API Reference

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

PropTypeDefault
customersrequired

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

Customer[]none
label

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

string"Customers"
pageSize

Customers per page.

number10
currency

ISO currency code for revenue, e.g. "USD" or "EUR".

string"USD"
locale

Locale for numbers and dates, e.g. "en-GB". Defaults to en-US while rendering on the server and during hydration, then the visitor's language.

stringnone
isLoading

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

booleanfalse
error

A message to show instead of the list when loading failed.

stringnone
onRetry

Adds a Try again button to the error message.

() => voidnone
insights

The totals and chart above the table. "auto" works them out from the customers and follows the filters (not with serverSide); "none" hides them; or pass your own numbers.

"auto" | "none" | CustomerInsights"auto"
serverSide

The list lives on a server. The table then shows exactly the customers you pass, and asks for new ones through onQueryChange when the search, a filter, the sort or the page changes.

booleanfalse
totalCount

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

numbernone
onQueryChange

With serverSide or a controlled query: called with the new query (search, status, plan, sort, page, pageSize). With serverSide, typing in the search waits for a short pause first.

(query: CustomerQuery) => voidnone
query

Controlled query. Pass the search, filters, sort and page you keep somewhere else, such as the address bar (see customerQuerySchema), and update it in onQueryChange. Without it the table keeps its own.

CustomerQuerynone
defaultQuery

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

Partial<CustomerQuery>none
onExport

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

(customers: Customer[], query: CustomerQuery) => voidnone
onOpenCustomer

The older name of onSelect: makes each name a button and calls this with the customer, e.g. to open a details panel. Both are called when both are given.

(customer: Customer) => voidnone
onSelect

Makes each name a button and calls this with the customer, e.g. to open a details panel. The conventional name; fires together with onOpenCustomer.

(customer: Customer) => voidnone
onSearch

Called with the search text whenever the search changes (after the short pause with serverSide). It fires in addition to onQueryChange.

(query: string) => voidnone
onFilter

Called with the chosen statuses and plans whenever a filter changes or the filters are cleared. It fires in addition to onQueryChange.

(filters: { status: string[]; plan: string[] }) => voidnone
permissions

What the app allows, by action: export (the Export CSV button) and open (opening a customer from a name). Each answer is "allow", "disabled", "hidden", true, false or { state, reason }. Hidden removes Export; disabled keeps it focusable with aria-disabled and the reason in a tooltip and read after the button. A disabled open makes the names plain text. Search and filters are always allowed. This only changes what people see: the server must check again.

Permissions<"export" | "open">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, insights, stat, chart, toolbar, search, filters, export, table, cards, empty, error, footer, pagination.

Partial<Record<"root" | "insights" | "stat" | "chart" | "toolbar" | "search" | "filters" | "export" | "table" | "cards" | "empty" | "error" | "footer" | "pagination", string>>none
statusTones

Which badge color a status gets. The defaults are active green, trial blue, overdue amber, churned red; anything else is neutral.

Record<string, "neutral" | "info" | "success" | "warning" | "danger">none
density

Row spacing of the table.

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

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 names when onOpenCustomer is set, and the page buttons
  • In the table: arrow keys move between cells; Enter or Space on a column header sorts
  • In a filter: Enter or Space opens the list, arrow keys choose, Escape closes
  • Escape in the search box clears it

Screen readers announce

  • "Customers, region" then the totals, then "Search customers, search edit text"
  • After typing: "3 customers" (polite)
  • The table reads as "Customers, table, 24 rows, 5 columns"; a cell reads its row header, column and value
  • A sort reads "Monthly revenue, column header, sorted descending"

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 customer), sortable column headers that expose their sort direction, and right-aligned money in tabular numerals
  • The count of matching customers is a polite status message, so filtering is announced; so is an export ("Exported 24 customers to customers.csv")
  • Search and filters each have a name (read, not drawn); the status of a customer is always text in a badge, never a color alone
  • Loading marks the region busy and shows skeletons instead of fake numbers; the controls are disabled while loading
  • An empty list says why (nothing yet, or nothing matches) and offers Clear filters; an error is an alert with a retry button
  • Below tablet width the table is replaced by cards (a list); only one of the two is in the accessibility tree at a time
  • The totals and chart come from the Stat and Chart components, so they are readable, keyboard-reachable and have a table view
  • CSV export neutralises cells that start with = + - or @ so a spreadsheet can't run them as formulas

Block contract

Data
A list of customers, or one page of them when serverSide is on, with a plain status tone map.
Data states
loading, empty, error, ready
Permissions
export, open
Events
onQueryChange, onSearch, onFilter, onSelect, onOpenCustomer, onExport, onRetry
You can replace
statusTones; insights; density; currency and locale; the parts it is built from (Table, Pagination, Chart, Select)

Guidelines

Use it when

  • An admin or billing screen that lists people or accounts with a plan, a status and revenue
  • You want search, filters, sorting, paging 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 list with different columns: build it from Table, Pagination and the other parts, as this block does

Don't

  • Loading every customer into the browser when there are many thousands: use serverSide
  • Showing the totals of one page of a server's list as if they were the totals of all customers
  • UI permission is not security: hiding or disabling Export or opening a customer only changes what people see, so the server must check access again for every request

Design tokens

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

  • --rd-color-surface-default
  • --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