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.
In React, listen with prop callbacks (onPageChange, onSelectRow, onQueryChange, …).
In Vue, listen with @event-name bindings (@update:page, @row-select, @sort, …). React-style callback props are also accepted and fire in addition to the emitted events, so migrating from React is drop-in.
Loading Code Block...Full example
Section titled “Full example”Loading Code Block...States
Section titled “States”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.
Loading
Section titled “Loading”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...Empty (no data)
Section titled “Empty (no data)”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...Filtered empty (no results)
Section titled “Filtered empty (no results)”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...Selectable rows
Section titled “Selectable rows”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...Row actions (dropdown menu)
Section titled “Row actions (dropdown menu)”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...Data props
Section titled “Data props”| Props | Default | Type | Description |
|---|---|---|---|
| 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. |
Header props
Section titled “Header props”The header row is only rendered when at least one of title, description, tableHeaderButtons, or filter is truthy.
| Props | Default | Type | Description |
|---|---|---|---|
| title | - | string | Heading shown above the table (typography preset applied). |
| description | - | string | Sub-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). |
| filter | false | boolean | When true, renders a search input in the header. Typing into it drives `queryState.search` and (in Vue) `@update:search`. |
Display & layout props
Section titled “Display & layout props”| Props | Default | Type | Description |
|---|---|---|---|
| numbered | false | boolean | When true, prepends an auto-incrementing `#` column that shows the row's 1-based index within the current page. |
| selectable | false | boolean | Renders a checkbox column and a 'select all' checkbox in the header. Enables `onSelectRow` / `@row-select`. |
| spaced | false | boolean | Adds extra vertical padding to rows for a roomier layout. Off by default for a denser data-grid feel. |
| outlined | true | boolean | Wraps the table in a bordered container with rounded corners. Set to false for a flush-flat layout inside a card. |
| loading | false | boolean | Renders a skeleton shimmer in place of rows. Preserves header + pagination chrome so the layout doesn't shift while data loads. |
Pagination props
Section titled “Pagination props”The Table embeds the Pagination component. When paginated is true these props flow straight through to it.
| Props | Default | Type | Description |
|---|---|---|---|
| paginated | true | boolean | When 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. |
| page | 1 | number | Initial (or controlled) 1-based page index. Sorting, searching, or changing the page size resets it to 1. |
| pageSize | 10 | number | Number of rows rendered per page. When the user picks a new size from the selector, `onPageSizeChange` fires and the page resets to 1. |
| totalRecords | data.length | number | Total 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. |
| showPageSize | true | boolean | Toggles the page-size selector inside the pagination footer. |
Data mode
Section titled “Data mode”| Props | Default | Type | Description |
|---|---|---|---|
| lazy | false | boolean | Enables 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`. |
Empty state props
Section titled “Empty state props”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.
| Props | Default | Type | Description |
|---|---|---|---|
| emptyStateTitle | 'No data found' | string | Title of the empty state shown when `data` is empty. |
| emptyStateDescription | 'Please try a different filter' | string | Body copy of the empty state shown when `data` is empty. |
| emptyStateButtonText | '' | string | Optional CTA label in the empty state. When empty, no button is rendered. |
| emptyStateImage | - | string | Optional custom illustration for the empty state. Falls back to the default EmptyState illustration when omitted. |
| emptyStateFilteredTitle | 'No results found' | string | Title of the *filtered* empty state — shown when data exists but nothing matches the current filters/search. |
| emptyStateFilteredDescription | 'Please adjust your filters or search term' | string | Body copy of the filtered empty state. |
| emptyStateFilteredButtonText | 'Clear filters' | string | CTA label in the filtered empty state. Defaults to 'Clear filters'. |
| emptyStateFilteredImage | - | string | Optional custom illustration for the filtered empty state. |
Column object
Section titled “Column object”Each entry in the columns array uses the following shape. At minimum you need header and either key or accessor.
| Props | Default | Type | Description |
|---|---|---|---|
| header | - | ReactNode | string | Column 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) => ReactNode | Custom value resolver. When provided, takes precedence over `key`. Return primitive values for text cells or nodes/VNodes to render arbitrary markup. |
| type | 'text' | CellType | How the cell is rendered — `text`, `image`, `button`, `link`, `phone`, `currency`, `chips`, `date`, or `action`. See the **CellType reference** table below. |
| currency | - | string | Currency code (e.g. `'GHS'`, `'USD'`) used when `type: 'currency'`. Formats the numeric value with the appropriate symbol. |
| imageKey | - | string | Row 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 | - | string | Chip colour token used when `type: 'chips'`. Maps to the semantic Chips colour (e.g. `'success'`, `'danger'`). |
| sortable | false | boolean | When true, the column header becomes clickable and toggles between ascending / descending order. Firing behaviour: emits `onQueryChange` (React) and `@sort` + `@update:page 1` (Vue). |
| wrap | false | boolean | Allows cell content to wrap onto multiple lines. Cells default to a single line with ellipsis. |
| onClick | - | (row: any) => void | Cell-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 }) => ReactNode | Custom cell renderer used when `actionsType` is `'custom'` or `'slot'`. Receives the row and returns arbitrary content. |
CellType reference
Section titled “CellType reference”The column.type field controls how each cell is rendered:
| Value | Default | Renders | Notes |
|---|---|---|---|
| 'text' | - | Plain string / number | Default. Renders the raw value from `key` / `accessor` with the standard body typography. |
| 'image' | - | Rounded image + text | Renders an image using `column.imageKey` (URL source) alongside the main text value. |
| 'button' | - | Inline text button | Renders a button using `column.actions`. Typically paired with `actionsType: 'button'`. |
| 'link' | - | Anchor | Renders the cell as a styled hyperlink. Combine with `column.onClick` for click handling. |
| 'phone' | - | Formatted phone number | Formats the raw digits into a readable phone-number string. |
| 'currency' | - | Currency-formatted number | Formats the numeric value using `column.currency` (e.g. `GHS 1,234.00`). |
| 'chips' | - | Chip / badge | Renders a Chip using the row value as label. Use `column.budgeVariant` to select the semantic colour. |
| 'date' | - | Localised date | Formats the raw value into a locale-aware date string. |
| 'action' | - | Actions cell | Renders a row-actions cell driven by `column.actionsType`, `column.actions`, `column.options`, or `column.customContent`. |
ActionsType
Section titled “ActionsType”Shape of each entry in column.actions (used by actionsType: 'button' | 'icons').
| Props | Default | Type | Description |
|---|---|---|---|
| label | - | string | ReactNode | Visible label of the action button. Rendered as the button text when `actionsType: 'button'`, or as a tooltip / a11y label when `actionsType: 'icons'`. |
| icon | - | ReactNode | Optional 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. |
| outlined | false | boolean | Renders the button with an outlined treatment when in `'button'` mode. |
| disabled | false | boolean | Disables the individual action, applying reduced opacity and preventing `onClick` from firing. |
| onClick | - | (row: any) => void | Handler invoked when the action is triggered. Receives the row the action was invoked on. |
TableOptionActionType
Section titled “TableOptionActionType”Shape of each entry in column.options (used by actionsType: 'options', i.e. the actions dropdown menu).
| Props | Default | Type | Description |
|---|---|---|---|
| label | - | string | ReactNode | Visible label of the menu item. |
| icon | - | ReactNode | Optional leading icon rendered before the label. |
| disabled | false | boolean | Disables the individual menu item, applying muted styling and blocking `onClick`. |
| onClick | - | (row: any) => void | Handler invoked when the menu item is selected. Receives the row the option was invoked on. |
TableButtonProps
Section titled “TableButtonProps”Shape of each entry in tableHeaderButtons (buttons rendered on the trailing edge of the table header).
| Props | Default | Type | Description |
|---|---|---|---|
| label | - | string | Required. Text rendered inside the header button. |
| variant | - | 'primary' | 'secondary' | Required. Visual style of the header button. |
| onClick | - | () => void | Required. 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.
Table-level callbacks
Section titled “Table-level callbacks”Column-level callbacks
Section titled “Column-level callbacks”Each Column object in the columns array can define its own callbacks that only apply to that column’s cells.
| Props | Default | Type | Description |
|---|---|---|---|
| column.onClick | - | (row: any) => void | Fires 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.sortable | false | boolean | When 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) => void | Fires 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) => void | Fires when a menu item inside an actions-dropdown (rendered via `actionsType: 'options'`) is selected. Receives the row the option was invoked on. |
Vue events
Section titled “Vue events”The Vue component exposes the same interactions as first-class emits, in addition to the callback props above.
| Event | Payload | Description | |
|---|---|---|---|
| @update:page | 1-based | (page: number) => void | Emitted whenever the active page changes — pagination, sort, search, or page-size change (each of which resets to page 1). Compatible with `v-model:page` for two-way binding. |
| @update:pageSize | - | (size: number) => void | Emitted when the user picks a new page size. Always fires together with `@update:page 1`. Compatible with `v-model:pageSize`. |
| @update:search | '' | (search: string) => void | Emitted when the header search input changes. Fires together with `@update:page 1` when the search moves the table off page 1. |
| @update:filters | {} | (filters: Record<string, any>) => void | Emitted when the active column filters change. Payload is the new filter object keyed by column key. |
| @sort | - | (key: string, direction: 'asc' | 'desc') => void | Emitted when the user clicks a `sortable` column header. Payload is the column key and the resulting direction. Fires together with `@update:page 1` when sorting moves the table off page 1. |
| @row-select | [] | (selectedRows: any[]) => void | Emitted whenever the selection set changes — per-row checkbox, header 'select all', or clearing. Payload is the array of currently selected row objects. Only fires when `selectable` is true. |
| @filters-cleared | - | () => void | Emitted when the user clicks 'Clear filters'. All internal filter, search, and sort state has already been reset by the time this fires. |
QueryState shape
Section titled “QueryState shape”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;}Callback firing order
Section titled “Callback firing order”Multiple callbacks can fire from a single interaction. The consistent order is:
- The specific callback for the interaction (
onPageChange,onSelectRow,onFiltersClear,@sort, …). - The aggregate
onQueryChange(React) — always fires when internal state changes, on the next render. - 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 })
Lazy (server-driven) mode
Section titled “Lazy (server-driven) mode”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)}/>