SearchableTableBlock
A records table with configurable columns, search, sortable headers, selection and pagination.
Import
import { SearchableTableBlock } from "@raydenui/ui/blocks";
import type { SearchableTableColumn, SearchableTableRow } from "@raydenui/ui/blocks";Usage
Each row is a Record<string, unknown> that must carry a string id. Columns look up
values by key.
const columns: SearchableTableColumn[] = [
{ key: "name", label: "Name", sortable: true },
{ key: "category", label: "Category" },
{ key: "price", label: "Price", sortable: true, width: "140px" },
];
const rows: SearchableTableRow[] = [
{ id: "sku-1", name: "Everyday ceramic cup", category: "Objects", price: "ÂŁ24.00" },
];
<SearchableTableBlock columns={columns} rows={rows} searchPlaceholder="Search products…" />;Local or consumer-owned search and sort
Search and sort are local by default. Supplying a handler moves that concern to you:
- Supply
onSearchand the block stops filteringrowsitself; it renders exactly what you pass. - Supply
onSortand the block only renders the sort indicator; you reorderrows.
The two halves cannot be mixed for one concern.
<SearchableTableBlock
columns={columns}
rows={remoteRows}
onSearch={(query) => search(query)}
onSort={(key, direction) => reorder(key, direction)}
/>Two toolbars
Passing title renders a title-and-icon toolbar. Omitting it renders a search-input
toolbar. In the title variant, the search and sort buttons render only when
onSearchClick and onSortClick are supplied.
<SearchableTableBlock
title="Products"
columns={columns}
rows={rows}
onSearchClick={() => openSearchPanel()}
onSortClick={() => openSortPanel()}
/>Filter and date selector need handlers
The filter button appears whenever onFilter is supplied; the date selector appears
whenever onDateSelect is supplied. showFilter and showDateSelector can hide them,
but setting either true without its handler does not bring it back — the block does not
render a control that has nowhere to go.
<SearchableTableBlock
columns={columns}
rows={rows}
onFilter={() => openFilterPanel()}
onDateSelect={() => openDatePicker()}
/><SearchableTableBlock
columns={columns}
rows={rows}
onFilter={() => openFilterPanel()}
showFilter={false}
/>Custom cell rendering
render receives the cell value first, then the whole row.
const columns: SearchableTableColumn[] = [
{ key: "name", label: "Name", sortable: true },
{
key: "actions",
label: "Actions",
render: (value, row) => (
<Button size="sm" variant="secondary" onClick={() => edit(String(row.id))}>
Edit
</Button>
),
},
];Selection
const [selectedIds, setSelectedIds] = useState<string[]>([]);
<SearchableTableBlock
columns={columns}
rows={rows}
selectable
selectedIds={selectedIds}
onSelectionChange={setSelectedIds}
/>;Pagination
const [page, setPage] = useState(1);
<SearchableTableBlock
columns={columns}
rows={pageRows}
page={page}
totalPages={totalPages}
onPageChange={setPage}
/>;Selection reconciliation contract
An uncontrolled selection covers only the rows the block is currently given. When
rows changes, any selected id that is no longer present is dropped permanently.
- Deleting a row deselects it; replacing rows with new ids clears the selection, so the header never reports a stale “all selected” over unchecked rows.
- A filter the block applies itself keeps the selection of rows that left view, because
the whole data set is still
rows. - A filter or page you apply outside the block hands over a different
rowsarray, so the selections that left with it are dropped. - Reconciliation is never silent: dropping ids fires
onSelectionChangewith what remains.
A controlled selection is never reconciled — supply selectedIds to keep a selection
across pages, or across a filter you apply yourself.
Full and partial selection are derived from membership of the rows on screen, never from set sizes.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | SearchableTableColumn[] | — | Required. Column definitions. |
rows | SearchableTableRow[] | — | Required. Each row needs a string id. |
title | string | — | Renders the title toolbar instead of the search-input toolbar. |
searchPlaceholder | string | — | Search input placeholder. |
onSearch | (query: string) => void | — | Supplying it puts search under your control. |
onSort | (key: string, direction: SortDirection) => void | — | Supplying it puts ordering under your control. |
onSearchClick | () => void | — | Title toolbar only. The button renders only when supplied. |
onSortClick | () => void | — | Title toolbar only. The button renders only when supplied. |
showFilter | boolean | — | Hides the filter button. Cannot show it without onFilter. |
onFilter | () => void | — | Required for the filter button to appear. |
showDateSelector | boolean | — | Hides the date selector. Cannot show it without onDateSelect. |
onDateSelect | () => void | — | Required for the date selector to appear. |
selectable | boolean | false | Enable row selection checkboxes. |
selectedIds | string[] | — | Controlled selection. Never reconciled. |
onSelectionChange | (ids: string[]) => void | — | Fired with the full resulting selection, including on reconciliation. |
onRowAction | (rowId: string) => void | — | Row kebab menu handler. The column renders only when supplied. |
page | number | — | Current page, 1-indexed. |
totalPages | number | — | Total pages. |
onPageChange | (page: number) => void | — | Page change handler. |
className | string | — | Additional class names. |
SearchableTableColumn
| Member | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Unique column key, used to look up row values. |
label | string | Yes | Column header label. |
sortable | boolean | No | Enable sorting on this column. |
width | string | No | Fixed column width as a CSS string, e.g. "300px". |
render | (value: unknown, row: Record<string, unknown>) => ReactNode | No | Custom cell renderer. Value first, then the row. |
SearchableTableRow
Record<string, unknown> & { id: string }.
States
- default — the record table.
- no matches — local search matched nothing.
- partial selection — the select-all control reports
aria-checked="mixed". - no configured actions — filter, date selector, row actions and pagination are absent rather than inert.
There is no loading or error state. Render your own around the block, or pass an empty
rows array.
Accessibility
- Native table structure with column scopes and
aria-sorton sortable headers. - A keyboard-focusable, bounded horizontal scroll region for the table itself.
- Named selection checkboxes and named row action controls.
- A visually hidden “Actions” heading for the action column.
Known limitations
- Search and sort cannot be partly local and partly consumer-owned for the same concern.
- There is no built-in filter UI.
onFilteropens a panel you provide.