Skip to content

Table

The Table component renders a paginated data grid with built-in support for sorting, in-memory searching, per-column filters, row selection, empty states, and lazy (server-driven) mode. Almost every interaction the user can perform emits either a React callback prop or a Vue event — this page documents every one of them.

Loading Code Block...
Loading Code Block...

The Table has first-class support for the states you’ll hit while wiring it up to a real dataset. Each preview below sits in a 600px container so you can compare states at a glance.

Set loading to render a skeleton shimmer in place of the rows. The header row and pagination footer stay in place so the layout doesn’t shift when data arrives.

Loading Code Block...

When data is empty, the Table renders the default empty state inside the body. Every string, illustration, and CTA is overridable via emptyStateTitle, emptyStateDescription, emptyStateButtonText, emptyStateButtonOnClick, and emptyStateImage.

Loading Code Block...

When there IS data but the current search / filters exclude every row, the Table swaps to the filtered empty state instead. Every string here is separately overridable via emptyStateFilteredTitle, emptyStateFilteredDescription, emptyStateFilteredButtonText, and emptyStateFilteredButtonOnClick. Type a nonsense word (e.g. zzz) into the search box in the preview below to trigger it.

Loading Code Block...

Set selectable to render a checkbox column and a ‘select all’ checkbox in the header. Selection changes fire onSelectRow (React) / @row-select (Vue) with the full array of selected row objects.

Loading Code Block...

Add a column with type: "action" and actionsType: "options" to render a per-row dropdown menu at the trailing edge. Each menu item receives the row when it’s activated.

Loading Code Block...

PropsDefaultTypeDescription
data[]any[]Array of row objects rendered by the table. Each row is a plain object; column cells read values from it either by `column.key` or via `column.accessor`.
columns-Column[]Column definitions. Order controls the visible order of columns. See the **Column object** table below for the full shape.

The header row is only rendered when at least one of title, description, tableHeaderButtons, or filter is truthy.

PropsDefaultTypeDescription
title-stringHeading shown above the table (typography preset applied).
description-stringSub-heading rendered below `title`, useful for a short explanation of the dataset.
tableHeaderButtons[]TableButtonProps[]Buttons rendered on the trailing edge of the header row. Each entry follows the `TableButtonProps` shape (see below).
filterfalsebooleanWhen true, renders a search input in the header. Typing into it drives `queryState.search` and (in Vue) `@update:search`.
PropsDefaultTypeDescription
numberedfalsebooleanWhen true, prepends an auto-incrementing `#` column that shows the row's 1-based index within the current page.
selectablefalsebooleanRenders a checkbox column and a 'select all' checkbox in the header. Enables `onSelectRow` / `@row-select`.
spacedfalsebooleanAdds extra vertical padding to rows for a roomier layout. Off by default for a denser data-grid feel.
outlinedtruebooleanWraps the table in a bordered container with rounded corners. Set to false for a flush-flat layout inside a card.
loadingfalsebooleanRenders a skeleton shimmer in place of rows. Preserves header + pagination chrome so the layout doesn't shift while data loads.

The Table embeds the Pagination component. When paginated is true these props flow straight through to it.

PropsDefaultTypeDescription
paginatedtruebooleanWhen true, renders the pagination footer. Set to false to show the entire dataset in a single scrolling view.
paginationVariant'default''default' | 'last-number'Which Pagination variant to render — the default numbered strip, or the `1 … N` last-number variant with a middle dropdown.
page1numberInitial (or controlled) 1-based page index. Sorting, searching, or changing the page size resets it to 1.
pageSize10numberNumber of rows rendered per page. When the user picks a new size from the selector, `onPageSizeChange` fires and the page resets to 1.
totalRecordsdata.lengthnumberTotal row count used by the pagination footer. When `lazy` is true you MUST pass this from your server response — otherwise the table can't compute the last page.
showPageSizetruebooleanToggles the page-size selector inside the pagination footer.
PropsDefaultTypeDescription
lazyfalsebooleanEnables server-driven mode — the Table skips its built-in search, sort, and pagination, and renders `data` as-is. Combine with `onQueryChange` to drive fetching from your backend and pass the paginated slice back via `data` + `totalRecords`.

The Table has two distinct empty states — no data (dataset is genuinely empty) and no results (dataset has rows but the current filters/search hide them all). Each is fully customisable.

PropsDefaultTypeDescription
emptyStateTitle'No data found'stringTitle of the empty state shown when `data` is empty.
emptyStateDescription'Please try a different filter'stringBody copy of the empty state shown when `data` is empty.
emptyStateButtonText''stringOptional CTA label in the empty state. When empty, no button is rendered.
emptyStateImage-stringOptional custom illustration for the empty state. Falls back to the default EmptyState illustration when omitted.
emptyStateFilteredTitle'No results found'stringTitle of the *filtered* empty state — shown when data exists but nothing matches the current filters/search.
emptyStateFilteredDescription'Please adjust your filters or search term'stringBody copy of the filtered empty state.
emptyStateFilteredButtonText'Clear filters'stringCTA label in the filtered empty state. Defaults to 'Clear filters'.
emptyStateFilteredImage-stringOptional custom illustration for the filtered empty state.

Each entry in the columns array uses the following shape. At minimum you need header and either key or accessor.

PropsDefaultTypeDescription
header-ReactNode | stringColumn header content. Accepts plain strings or arbitrary nodes (e.g. an icon + label).
key-string | string[]Path(s) into the row object used to read the cell value. A string is a single field name; a string array joins nested values (useful for split cells with title + subtitle).
accessor-(row: any) => ReactNodeCustom value resolver. When provided, takes precedence over `key`. Return primitive values for text cells or nodes/VNodes to render arbitrary markup.
type'text'CellTypeHow the cell is rendered — `text`, `image`, `button`, `link`, `phone`, `currency`, `chips`, `date`, or `action`. See the **CellType reference** table below.
currency-stringCurrency code (e.g. `'GHS'`, `'USD'`) used when `type: 'currency'`. Formats the numeric value with the appropriate symbol.
imageKey-stringRow field to read the image URL from when `type: 'image'`.
subKey-string | string[]Path(s) for a secondary line rendered under the main value (title + subtitle cells).
budgeVariant-stringChip colour token used when `type: 'chips'`. Maps to the semantic Chips colour (e.g. `'success'`, `'danger'`).
sortablefalsebooleanWhen true, the column header becomes clickable and toggles between ascending / descending order. Firing behaviour: emits `onQueryChange` (React) and `@sort` + `@update:page 1` (Vue).
wrapfalsebooleanAllows cell content to wrap onto multiple lines. Cells default to a single line with ellipsis.
onClick-(row: any) => voidCell-level click handler. Fires with the full row object when any cell in this column is clicked. Actions / checkbox cells are exempt.
actionsType-'options' | 'button' | 'custom' | 'slot' | 'icons'Only meaningful when `type: 'action'`. Selects how the actions column renders: `'options'` → dropdown menu, `'icons'` → row of icon buttons, `'button'` → inline text button(s), `'custom'` / `'slot'` → user-provided renderer.
actions-ActionsType[]Buttons rendered when `actionsType` is `'button'` or `'icons'`. See the **ActionsType** table below.
options-TableOptionActionType[]Menu items rendered inside the actions dropdown when `actionsType` is `'options'`. See the **TableOptionActionType** table below.
customContent-(props: { row: any }) => ReactNodeCustom cell renderer used when `actionsType` is `'custom'` or `'slot'`. Receives the row and returns arbitrary content.

The column.type field controls how each cell is rendered:

ValueDefaultRendersNotes
'text'-Plain string / numberDefault. Renders the raw value from `key` / `accessor` with the standard body typography.
'image'-Rounded image + textRenders an image using `column.imageKey` (URL source) alongside the main text value.
'button'-Inline text buttonRenders a button using `column.actions`. Typically paired with `actionsType: 'button'`.
'link'-AnchorRenders the cell as a styled hyperlink. Combine with `column.onClick` for click handling.
'phone'-Formatted phone numberFormats the raw digits into a readable phone-number string.
'currency'-Currency-formatted numberFormats the numeric value using `column.currency` (e.g. `GHS 1,234.00`).
'chips'-Chip / badgeRenders a Chip using the row value as label. Use `column.budgeVariant` to select the semantic colour.
'date'-Localised dateFormats the raw value into a locale-aware date string.
'action'-Actions cellRenders a row-actions cell driven by `column.actionsType`, `column.actions`, `column.options`, or `column.customContent`.

Shape of each entry in column.actions (used by actionsType: 'button' | 'icons').

PropsDefaultTypeDescription
label-string | ReactNodeVisible label of the action button. Rendered as the button text when `actionsType: 'button'`, or as a tooltip / a11y label when `actionsType: 'icons'`.
icon-ReactNodeOptional icon. Used as the sole content in `'icons'` mode, or as a leading icon in `'button'` mode.
variant-'primary' | 'secondary'Button visual style. Ignored in `'icons'` mode where the icon appearance takes over.
outlinedfalsebooleanRenders the button with an outlined treatment when in `'button'` mode.
disabledfalsebooleanDisables the individual action, applying reduced opacity and preventing `onClick` from firing.
onClick-(row: any) => voidHandler invoked when the action is triggered. Receives the row the action was invoked on.

Shape of each entry in column.options (used by actionsType: 'options', i.e. the actions dropdown menu).

PropsDefaultTypeDescription
label-string | ReactNodeVisible label of the menu item.
icon-ReactNodeOptional leading icon rendered before the label.
disabledfalsebooleanDisables the individual menu item, applying muted styling and blocking `onClick`.
onClick-(row: any) => voidHandler invoked when the menu item is selected. Receives the row the option was invoked on.

Shape of each entry in tableHeaderButtons (buttons rendered on the trailing edge of the table header).

PropsDefaultTypeDescription
label-stringRequired. Text rendered inside the header button.
variant-'primary' | 'secondary'Required. Visual style of the header button.
onClick-() => voidRequired. Handler invoked when the header button is clicked. Header buttons do not receive a row context (they operate on the table as a whole).

Every interaction on the Table surfaces through the callbacks (React) or events (Vue) below. The Vue component also accepts every React-style callback prop as a fallback — the emitted event and the prop callback both fire.

Each Column object in the columns array can define its own callbacks that only apply to that column’s cells.

PropsDefaultTypeDescription
column.onClick-(row: any) => voidFires when a cell in this column is clicked. Receives the full row object. Useful for row-drilldown navigation while keeping the checkbox / actions cells inert.
column.sortablefalsebooleanWhen true, the column header becomes clickable and toggles between ascending and descending order. The sort triggers `onQueryChange` and (in Vue) `@sort`.
column.actions[].onClick-(row: any) => voidFires when an inline action button (rendered via `actionsType: 'button' | 'icons'`) is activated. Receives the row the action was invoked on.
column.options[].onClick-(row: any) => voidFires when a menu item inside an actions-dropdown (rendered via `actionsType: 'options'`) is selected. Receives the row the option was invoked on.

onQueryChange receives the complete internal query state. Use it as the single source of truth when driving a lazy (server-side) fetch.

interface QueryState {
sortDir: "asc" | "desc";
sortColumn: string;
filters: Record<string, any>;
pageIndex: number;
pageSize: number;
search: string;
}

Multiple callbacks can fire from a single interaction. The consistent order is:

  1. The specific callback for the interaction (onPageChange, onSelectRow, onFiltersClear, @sort, …).
  2. The aggregate onQueryChange (React) — always fires when internal state changes, on the next render.
  3. Any secondary onPageChange(1) that follows a sort / search / page-size change, since those actions reset to page 1.

Example — user changes page size from 10 to 25:

  • onPageSizeChange(25)
  • onPageChange(1) (implicit reset)
  • onQueryChange({ …, pageSize: 25, pageIndex: 1 })

Set lazy to skip client-side filtering/sorting/pagination and drive the table entirely from your server. In lazy mode you typically only need onQueryChange — every interaction produces one authoritative payload you can send to your API.

<Table
lazy
data={rowsFromServer}
totalRecords={totalFromServer}
onQueryChange={(state) => fetchFromServer(state)}
/>