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.
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
24 customers
Showing 1 to 8 of 24
Server side
Loading customers
Loading and empty
Loading customers
Custom insights
4 customers
Showing 1 to 4 of 4
Without insights
5 customers
Showing 1 to 5 of 5
Row action
3 customers
Showing 1 to 3 of 3
Url state
/customers
12 customers
Showing 1 to 5 of 12
Permissions
3 customers
Only admins can export customersShowing 1 to 3 of 3
ClassNames
3 customers
Showing 1 to 3 of 3
Installation
npx rdloom add customer-tableCopies 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.jsonWorks, 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.
| Prop | Type | Default |
|---|---|---|
customersrequiredThe customers to show. With serverSide, only the current page. | Customer[] | none |
labelAccessible name of the table and the section, e.g. "Customers". | string | "Customers" |
pageSizeCustomers per page. | number | 10 |
currencyISO currency code for revenue, e.g. "USD" or "EUR". | string | "USD" |
localeLocale 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. | string | none |
isLoadingShow loading rows and skeleton cards, disable the controls, and mark the section busy. | boolean | false |
errorA message to show instead of the list when loading failed. | string | none |
onRetryAdds a Try again button to the error message. | () => void | none |
insightsThe 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" |
serverSideThe 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. | boolean | false |
totalCountWith serverSide: how many customers match in all, so the pages and the count are right. | number | none |
onQueryChangeWith 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) => void | none |
queryControlled 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. | CustomerQuery | none |
defaultQueryStart with a search, filters, a sort or a page already set. | Partial<CustomerQuery> | none |
onExportReplaces 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) => void | none |
onOpenCustomerThe 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) => void | none |
onSelectMakes 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) => void | none |
onSearchCalled with the search text whenever the search changes (after the short pause with serverSide). It fires in addition to onQueryChange. | (query: string) => void | none |
onFilterCalled 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[] }) => void | none |
permissionsWhat 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 |
classNamesClass 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 |
statusTonesWhich 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 |
densityRow 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