Table
Build record tables with sorting, search, filters, pagination, selection and row actions.
Use Table for records people find, open and act on. Start with rows, typed columns, and stable row identities, then add the features your screen needs. Use DataGrid for financial matrices with grouped headers and keyboard cell navigation.
Headers and alignment
Headers are 36px high with 12px medium labels and 12px icons. Hover or focus an unsorted header to reveal the neutral sort indicator; the active column keeps its direction visible. Click to cycle sorting. End-aligned numeric columns place sorting before the label. Supply description for a separate, keyboard-accessible information tooltip. Selection headers use the same horizontal inset as row checkboxes.
Usage
'use client';
import { Table } from '@commenda/pathfinder/table';
const invoices = [
{ id: 'inv-001', number: 'INV-001', customer: 'Meridian UK Ltd' },
{ id: 'inv-002', number: 'INV-002', customer: 'Meridian US Inc.' },
];
export default function InvoicesTable() {
return (
<Table
label="Invoices"
rows={invoices}
rowConfig={{ getId: (invoice) => invoice.id }}
columns={[
{ id: 'number', header: 'Invoice', value: (invoice) => invoice.number },
{
id: 'customer',
header: 'Customer',
value: (invoice) => invoice.customer,
},
]}
/>
);
}Import @commenda/pathfinder/styles.css once in your app layout. Pathfinder ships its own compiled styles. Open Code on any example for the complete, formatted implementation; the snippet comes from the same source as its preview.
Columns define a stable id, a visible header, and a value(row) accessor. Add cell(row) when rendering richer content. Keep the accessor when sorting or searching should use the underlying value rather than its formatted label. rowConfig.getId must return a stable record ID, not an array index.
Sorting
Click Amount to cycle descending, unsorted and ascending order. The initial sort is controlled by sortingConfig; the status below the example reflects the current value. Numeric accessors sort numerically even when the cell formats money. Local sorting runs before pagination.
Search and filters
Open search and enter Meridian UK, or choose Pending from Status. Search and filters combine. Search opens in place; tools stay together and excess filter buttons move into +N. A query with no matches offers Clear filters. This example supplies getText explicitly so invoice numbers and customer names are searchable.
For a local collection, matches(row, values) defines filter membership. In remote mode the server applies it; use controlled search/filter state as request inputs. Reset the page when an externally controlled filter changes.
Single-choice filters
Choose a status. Single choice replaces the previous selection, shows a checkmark and closes its menu. Clear resets that field. The selected value becomes an editable chip below the toolbar.
Complex filters
This implements Option A from Figma: one compound chip per active filter, with field · operator · value · remove. Change Status from “is either of” to “is not”, search Entity options, or open Due date to choose a preset or calendar range. Use More filters to add Assignee or Amount; the field editor opens immediately. Resize the container to reveal the +N filter overflow menu.
The toolbar keeps quiet field buttons. Applied chips wrap below it. Multiple choices stay checked while searching, selected options appear first, and country flags lead entity names. Each menu clears only its own field; Clear filters resets the whole filter map. Date changes apply once a complete range exists. Numeric conditions apply after validation.
Fields combine with AND; multiple selected choices within one field combine with OR. “is not” negates membership. The app controls the value map and can control operators independently. For remote tables, serialize both into your API request and query-selection key; the table never re-filters a server page. Missing dates/numbers do not match a numeric/date condition. Arbitrarily nested AND/OR expressions are not part of this field-filter API.
Pagination
Move through twelve records in pages of five, then change Rows per page. The example controls { page, pageSize } and offers 5/10 for demonstration. Defaults are page 1, 25 rows, and 25/50/100 choices. Filtering and sorting apply to the full local collection before it is sliced.
Column visibility
Open Columns and enable Amount. Invoice cannot be hidden because it identifies the record. The controlled viewConfig.hiddenColumns array can be stored in application preferences; column IDs should survive label changes and translations. Density and visibility can be controlled independently.
Column pinning
Scroll horizontally: Invoice stays at the logical start and Amount at the end. Open Columns, then a column's pin control to move it or unpin it. Selection and action cells remain at their edges alongside pinned data. Hidden columns do not leave gaps. Pinning uses logical edges in RTL and releases in a narrow container when the pinned regions would obscure readable data.
Use column.pin for a fixed default or viewConfig.columnPinning for interactive preferences. Its value is a map of column IDs to 'start', 'end', or null (explicitly unpinned). pinnable: false locks a column's configured position.
Drag column widths
Drag a header's trailing edge to increase or decrease its width. Focus the handle to use Left/Right in 10px steps, Shift for 50px, or Home/End for the bounds. Escape during a drag restores its starting width. Resize does not trigger sorting; pinned offsets follow the updated widths.
Enable handles with viewConfig.columnWidths. Set each column's width, minWidth, maxWidth, and optionally resizable: false. The default bounds are 80–800px; choose bounds appropriate to the content, especially money and dates. Controlled width and pin maps can be saved in application preferences. Explicit layout widths preserve accurate drag distances and pin offsets; a table can leave unused space when its configured total is narrower than its container.
Row selection
Select an unpaid invoice, move to the next page, or choose Select all 3 in the bulk bar. Paid invoices are ineligible. Selection persists by stable ID. Request reminder updates the status message only; this demo does not send messages.
TableSelection is { kind: 'ids', ids } or { kind: 'all', queryKey, excludedIds }. The header checkbox selects the current eligible page. allMatching explicitly enables the entire eligible query, including unloaded pages. Pass a new query key when filter membership or authorization scope changes. The server must interpret that query/exclusion model and recheck permissions.
Row actions and copying
Click a record to open it, use its overflow menu, or hover/focus the invoice number to copy it. Those controls do not also activate the row. Download receipt is unavailable before payment. The example reports requested actions locally; real navigation/downloads belong to the application.
Use specific action names and at most a primary icon, secondary icon and overflow per row. Keep whole-record editing in a Sheet; reserve inline editing for frequently changed fields.
Custom cells
This filing table combines descriptions, country flags, assignees, status badges and row actions in a real record layout. cell(row) composes existing Pathfinder components without changing the underlying record shape. Entity names use flags; people use Avatar. System status uses Badge, while selected removable values use Tag.
Text aligns at the leading edge, numbers at the trailing edge, and badges align with their header. Missing values use an en dash, not zero. Use TableNumberCell for consistent decimals and ISO currency labels. Long names can grow a row rather than lose essential content.
Grouped records
Group filings by entity with a flag, count and read-only summary. Activate a group header to collapse it. Selection survives collapse, and the header checkbox still selects eligible records on the current page. Search, sort and pagination operate before grouping; page counts describe records, never group headers. A large entity may span pages. The summary callback receives the current page's group records; pass app-owned aggregates if the summary must describe the entire query.
Use groupingConfig.getId, getLabel, optional leading, summary, and controlled/uncontrolled collapsedGroups. For recursive financial rows, use DataGrid hierarchy.
Toolbar actions
The right side contains tools, overflow, a divider, then at most one secondary and one primary action. Export becomes an icon when actions are present. A primary action with menu renders a SplitButton: the main button performs the default action, while the arrow exposes alternatives. Selection actions belong in the bulk bar.
Use named primaryAction and secondaryAction; put extra actions in moreMenu. These examples report callbacks locally. Your application supplies upload, export and navigation workflows.
Documents
File icons identify documents, country flags identify entities, and avatars identify uploaders. References expose a copy action on hover, focus or touch. The demo writes the selected reference to the clipboard and reports failures.
Team members
People include their email, role as a Tag, account status as a Badge and an accessible yes/no symbol for two-factor authentication. Missing activity is an en dash. Row-specific actions live in the overflow menu.
Density and containers
Use the view menu to compare compact and relaxed density, and the button to switch card/flat containers. The second record includes a description to show the two-line behavior.
| Measurement | Compact | Relaxed |
|---|---|---|
| Header | 36px | 36px |
| Single-line row minimum | 40px | 52px |
| Two-line text row | 52px | 64px |
| Horizontal cell padding | 12px | 12px |
A card adds a 12px radius and hairline border. Use flat inside an existing surface. Wrapped content, text enlargement and taller custom controls can increase these heights.
Presets and summary
Choose Overdue, then clear its toolbar filter. Preset cards and filters share state, so the selected preset updates with the query. The summary is read-only. Cards sit above the table; summary content lives inside it.
This example's summary follows the status filter. Production summaries should use aggregates for the same reporting query, including search when applicable, rather than summing one loaded API page. Preset definitions, counts and persistence are app-owned.
Editable cells and totals
Hover a row, click an account or amount, and edit it. Enter saves, Escape cancels, and Add line appends a record. Totals recompute from application data and align by column ID through footerConfig.total.cells. The first visible column contains the total label.
The example saves in memory. An async onCommit must resolve only after persistence succeeds and reject on failure; the editor retains rejected drafts and offers Retry save. Text/number fields use 28px compact geometry. Select and DatePicker preserve their existing accessible dimensions and can increase row height.
Account mapping and validation
This account-mapping example combines Select and numeric editors, an existing invalid allocation, aligned money totals and Add line. Try Keep fields outlined for form-style tables. Simulate save failure retains the draft and exposes retry; disable the simulation to complete the save. The app owns validation and financial calculations.
Dates, switches and attachments
Compose DatePicker editing, an immediate Switch and an upload action directly in cells. The picker keeps Pathfinder's approved DateField geometry. Once a PDF is chosen, its filename replaces the upload action. File selection is local in this example; no bytes leave the browser. Real uploads need application-side file validation, progress, persistence and failure handling.
API-controlled data
dataMode="remote" displays exactly the rows returned by the app. Table never sorts, filters or slices that received page again. The example uses synchronous fixture filtering to demonstrate state ownership, not a production API adapter.
Use search, filters, sort and pagination as a request key. Keep typing responsive, debounce requests rather than the displayed value, cancel obsolete requests, and prevent stale responses from replacing newer results. Remote numbered pages require paginationConfig.totalRows. URL persistence, exports and server authorization remain application responsibilities.
Cursor pagination
Use Previous and Next without an invented total. The demo simulates server pages; real applications hold opaque API cursors. Supply hasNext, hasPrevious, callbacks and an honest summary. Pair cursor configuration with dataMode="remote".
Loading, empty and error
Switch between loading skeletons, first-use emptiness and a retryable error. Search/filter emptiness instead offers Clear filters. Keep toolbar and column geometry stable while requests run. Retry should repeat the current query, not discard the user's scope.
Accessibility and responsive behavior
Table retains native table semantics. Checkbox selection is separate from row activation; a focused activatable row opens with Enter. Search shortcuts apply while focus is in table tools. The scroll region supports keyboard access. Narrow layouts scroll within the component and release pinned columns if they would hide readable values.
Read the Figma rules and explicit differences before changing dimensions or behavior.
API reference
groupingConfig groups the current page: { getId(row), getLabel(id), leading?(id), summary?(id, pageRows), collapsedGroups? }. Collapsed state is a list of group IDs using the same value / onValueChange or defaultValue convention as other preferences.
TableFilter is discriminated by type:
| Prop | Type | Behavior |
|---|---|---|
labelRequired | string | Visible field label. Supply an accessible name when it is omitted. |
rowsRequired | readonly TRow[] | — |
columnsRequired | readonly TableColumn<TRow>[] | — |
rowConfigRequired | ObjectView fields | — |
viewConfig | ObjectView fields | — |
groupingConfig | ObjectView fields | — |
container | "card"|"flat" | — |
sortingConfig | ObjectView fields · 2 shapes | — |
searchConfig | SelectionProps<string> & {
getText?:
| ((row: TRow) => string)
| undefined;
placeholder?: string;
} | — |
filterConfig | ObjectView fields | — |
selectionConfig | View full typeSelectionProps<TableSelection> & {
canSelect?:
| ((row: TRow) => boolean)
| undefined;
allMatching?: {
queryKey: string;
total: number;
};
actions?: readonly TableAction[];
moreMenu?: DropdownMenu;
} | — |
toolbarConfig | TableToolbarConfigView fields | — |
stateConfig | TabularStateConfigView fields | — |
summary | ReactNode | — |
footerConfig | ObjectView fields | — |
className | string | Additional classes for the component root. |
dataModeConditional | "local"|"remote" | — |
paginationConfigConditional | ObjectView fields | — |
rowConfigObject fields
| Field | Type | Requirement |
|---|---|---|
getId | (row: TRow) => string | Required |
getLabel | (row: TRow) => string | Required |
onActivate | ((row: TRow) => void) | Optional |
menu | ((row: TRow) => DropdownMenu) | Optional |
viewConfigObject fields
| Field | Type | Requirement |
|---|---|---|
density | Object | Optional |
hiddenColumns | Object | Optional |
columnWidths | Object | Optional |
columnPinning | Object | Optional |
viewConfig.densityObject fields
Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.
Option 1
| Field | Type | Requirement |
|---|---|---|
value | "compact" | "relaxed" | Required |
onValueChange | (value: "compact" | "relaxed") => void | Required |
Do not supply: defaultValue.
Option 2
| Field | Type | Requirement |
|---|---|---|
defaultValue | "compact" | "relaxed" | Optional |
onValueChange | ((value: "compact" | "relaxed") => void) | Optional |
Do not supply: value.
viewConfig.hiddenColumnsObject fields
Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.
Option 1
| Field | Type | Requirement |
|---|---|---|
value | readonly string[] | Required |
onValueChange | (value: readonly string[]) => void | Required |
Do not supply: defaultValue.
Option 2
| Field | Type | Requirement |
|---|---|---|
defaultValue | readonly string[] | Optional |
onValueChange | ((value: readonly string[]) => void) | Optional |
Do not supply: value.
viewConfig.columnWidthsObject fields
Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.
Option 1
| Field | Type | Requirement |
|---|---|---|
value | Readonly<Partial<Record<string, number>>> | Required |
onValueChange | (value: Readonly<Partial<Record<string, number>>>) => void | Required |
Do not supply: defaultValue.
Option 2
| Field | Type | Requirement |
|---|---|---|
defaultValue | Readonly<Partial<Record<string, number>>> | Optional |
onValueChange | ((value: Readonly<Partial<Record<string, number>>>) => void) | Optional |
Do not supply: value.
viewConfig.columnPinningObject fields
Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.
Option 1
| Field | Type | Requirement |
|---|---|---|
value | Readonly<Partial<Record<string, "start" | "end" | null>>> | Required |
onValueChange | (value: Readonly<Partial<Record<string, "start" | "end" | null>>>) => void | Required |
Do not supply: defaultValue.
Option 2
| Field | Type | Requirement |
|---|---|---|
defaultValue | Readonly<Partial<Record<string, "start" | "end" | null>>> | Optional |
onValueChange | ((value: Readonly<Partial<Record<string, "start" | "end" | null>>>) => void) | Optional |
Do not supply: value.
groupingConfigObject fields
| Field | Type | Requirement |
|---|---|---|
getId | (row: TRow) => string | Required |
getLabel | (id: string) => string | Required |
leading | ((id: string) => ReactNode) | Optional |
summary | ((id: string, pageRows: readonly TRow[]) => ReactNode) | Optional |
collapsedGroups | Object | Optional |
groupingConfig.collapsedGroupsObject fields
Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.
Option 1
| Field | Type | Requirement |
|---|---|---|
value | readonly string[] | Required |
onValueChange | (value: readonly string[]) => void | Required |
Do not supply: defaultValue.
Option 2
| Field | Type | Requirement |
|---|---|---|
defaultValue | readonly string[] | Optional |
onValueChange | ((value: readonly string[]) => void) | Optional |
Do not supply: value.
sortingConfigObject fields
Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.
Option 1
| Field | Type | Requirement |
|---|---|---|
value | Object | Required |
onValueChange | (value: TableSort | null) => void | Required |
Do not supply: defaultValue.
sortingConfig.valueObject fields
| Field | Type | Requirement |
|---|---|---|
columnId | string | Required |
direction | "desc" | "asc" | Required |
Option 2
| Field | Type | Requirement |
|---|---|---|
defaultValue | Object | Optional |
onValueChange | ((value: TableSort | null) => void) | Optional |
Do not supply: value.
sortingConfig.defaultValueObject fields
| Field | Type | Requirement |
|---|---|---|
columnId | string | Required |
direction | "desc" | "asc" | Required |
filterConfigObject fields
| Field | Type | Requirement |
|---|---|---|
filters | readonly TableFilter<TRow>[] | Required |
operators | Object | Optional |
value | Readonly<Record<string, readonly string[]>> | Required |
onValueChange | (value: Record<string, readonly string[]>) => void | Required |
filterConfig.operatorsObject fields
Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.
Option 1
| Field | Type | Requirement |
|---|---|---|
value | Readonly<Record<string, TableFilterOperator>> | Required |
onValueChange | (value: Readonly<Record<string, TableFilterOperator>>) => void | Required |
Do not supply: defaultValue.
Option 2
| Field | Type | Requirement |
|---|---|---|
defaultValue | Readonly<Record<string, TableFilterOperator>> | Optional |
onValueChange | ((value: Readonly<Record<string, TableFilterOperator>>) => void) | Optional |
Do not supply: value.
toolbarConfigObject fields
| Field | Type | Requirement |
|---|---|---|
actions | readonly TableAction[] | Optional Prefer named actions. Legacy actions use first=primary, second=secondary, remainder=overflow. |
primaryAction | (TableAction & { menu?: DropdownMenu; icon?: ReactNode; }) | Optional |
secondaryAction | Object | Optional |
onExport | (() => void) | Optional |
moreMenu | Object | Optional |
toolbarConfig.secondaryActionObject fields
| Field | Type | Requirement |
|---|---|---|
id | string | Required |
label | string | Required |
onAction | () => void | Required |
disabled | boolean | Optional |
toolbarConfig.moreMenuObject fields
| Field | Type | Requirement |
|---|---|---|
groups | DropdownGroup[] | Required |
stateConfigObject fields
| Field | Type | Requirement |
|---|---|---|
isLoading | boolean | Optional |
error | Object | Optional |
empty | Object | Optional |
stateConfig.errorObject fields
| Field | Type | Requirement |
|---|---|---|
message | string | Required |
onRetry | () => void | Required |
stateConfig.emptyObject fields
| Field | Type | Requirement |
|---|---|---|
title | string | Required |
description | string | Optional |
action | Object | Optional |
stateConfig.empty.actionObject fields
| Field | Type | Requirement |
|---|---|---|
id | string | Required |
label | string | Required |
onAction | () => void | Required |
disabled | boolean | Optional |
paginationConfigObject fields
| Field | Type | Requirement |
|---|---|---|
mode | "cursor" | Required |
hasNext | boolean | Required |
hasPrevious | boolean | Required |
onNext | () => void | Required |
onPrevious | () => void | Required |
summary | string | Optional |
pageSize | number | Required |
onPageSizeChange | ((size: number) => void) | Optional |
All filters have id, label, optional icon, and initiallyVisible: false for fields added through More filters. filterConfig.operators uses a controlled/uncontrolled map of is, isNot, isAnyOf, within, greaterThan or lessThan; menus expose only operators meaningful for that field type. Range filters use inclusive endpoints. A custom choice matches predicate describes positive membership; isNot is applied by Table.
| Prop | Type | Behavior |
|---|---|---|
label | string | Required accessible table name |
rows | readonly TRow[] | Immutable local collection or one received API page |
columns | readonly TableColumn<TRow>[] | IDs, headers, accessors and renderers |
rowConfig | { getId, getLabel, onActivate?, menu? } | Stable unique IDs; activation opens the record, checkbox selects it |
dataMode | 'local' or 'remote' | Local by default; remote never reprocesses the received page |
viewConfig | { density?, hiddenColumns?, columnWidths?, columnPinning? } | Each uses controlled value/onValueChange or defaultValue; compact and all columns visible by default |
container | 'card' or 'flat' | Card by default |
sortingConfig | SelectionProps<TableSort or null> | Controlled value/onValueChange or defaultValue |
searchConfig | { value?, defaultValue?, onValueChange?, getText?, placeholder? } | Enables search; default local searchable text joins column values |
filterConfig | { filters, value, onValueChange, operators? } | Controlled filter map; typed choice/date/number definitions, optional controlled operators |
paginationConfig | Page or cursor configuration | Local page 1 and 25 rows by default |
selectionConfig | SelectionProps plus selection options | Stable IDs, optional eligibility, query-scoped select-all and bulk actions |
toolbarConfig | { primaryAction?, secondaryAction?, onExport?, moreMenu?, actions? } | App callbacks; exports never imply just the loaded rows |
stateConfig | { isLoading?, error?, empty? } | Skeleton rows, retryable failure or empty state |
summary | ReactNode | Read-only summary inside the container; put app preset controls above Table |
footerConfig | { total?: { label, cells }, addAction? } | App-owned totals keyed by column ID and an explicit add-row action; first visible column contains the total label |
className | string | Root classes |
TableColumn has id, string header, optional description tooltip, value(row), cell(row), width, minWidth, maxWidth in pixels, resizable, pinnable, align: 'start' | 'center' | 'end', sortable, compare(left, right), hideable, and pin: 'start' | 'end'. value supplies sortable/searchable data even when cell renders rich content. Pinned columns are grouped at their logical edge; horizontal pinning releases when it would leave insufficient space for a value column. Do not hide every column. Column IDs and row IDs must remain stable, nonempty and unique when updating data. Invalid IDs, empty column collections and invalid page numbers fail with configuration errors.
viewConfig.density uses compact or relaxed; viewConfig.hiddenColumns uses column IDs. Control either independently to persist user preferences. Non-hideable columns stay visible, and the first column remains as a safeguard if all hideable columns were requested hidden.
TableSort is { columnId, direction: 'asc' | 'desc' }; the sort control cycles ascending, descending, unsorted. Local search/filter/sort run before pagination. Search and sort changes return to the first page. App-controlled filters should reset the page when changed externally.
Cell renderers
TableCell is a discriminated content component; the owning table supplies padding and height:
type | Content props |
|---|---|
text (default) | value, description, leadingIcon, badge, tooltip, struck |
entity / file | label, icon, optional description |
person | Avatar identity, optional description |
people | label, AvatarGroup people |
badge / tag | items using existing Badge/Tag props |
boolean | value, optional accessible label |
progress | existing ProgressBar progress props |
link | label, linkConfig |
copy | value, onCopy, optional copyLabel |
loading | Skeleton content |
number | All TableNumberCell props |
TableNumberCell accepts value, optional locale, Intl.NumberFormat format, unit, suffix, tone, struck and trend: { direction: 'up' | 'down', intent: 'positive' | 'critical' | 'neutral' }. Numbers align right without truncation; unit labels stay separate. Empty and non-finite numeric values show an en dash. Numeric strings preserve their supplied formatting. Trend direction does not determine business meaning: rising costs may be critical.
TableEditCell accepts label, string value, editor, optional display, outlined, disabled, validationMessage, validate(value) and async onCommit(value). Editors are { type: 'text', placeholder? }, { type: 'number', min?, max?, step?, unit?, suffix? }, { type: 'select', options, placeholder? }, or { type: 'date', min?, max? }. Number drafts remain strings; validate and parse at the application boundary. Enter or leaving the editor saves; Escape cancels. Failed saves retain the draft and show Retry save. Select/calendar portals belong to the same editing session. DatePicker retains the approved Kakashi DateField padding. DataGrid manages editingConfig internally; ordinary Table renderers use the local editing state.
Design rules
Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.
API references
The API review used shadcn's Data Table guide, TanStack controlled state and typed column definitions. Pathfinder adopts independently controlled state and reusable boundaries while retaining direct props, Figma styling and precompiled CSS. It keeps TanStack 8.21.3 implementation types private; the current shadcn guide's v9 code is not copied into this v8 integration. Matrix keyboard behavior follows the ARIA grid pattern.
Table
-
TABLE-001: White card, radius 12px, pearl-300 hairline 0.5px; optional flat container. Header 36px, compact row minimum 40px, relaxed 52px, horizontal padding 12px. Two-line text rows are 52px compact and 64px relaxed; longer wrapping and accessible controls can grow further. Toolbar and pagination minimum 48px.
-
TABLE-002: Hover and selected rows share pearl-200. Checkbox distinguishes selection; clicking the row opens the record. Links, editors, menus and checkboxes do not also activate the row. Enter opens a focused activatable row.
-
TABLE-003: Local processing is filter/search, sort, then page. Remote processing belongs entirely to the app; no double filtering, sorting or slicing. The app owns URL state, fetching, cancellation, permissions, exports and API cursors. Slash opens search while focus is within table tools. Escape clears then closes search. Search opens in place and its clear action clears and closes. Tools remain together; excess filter buttons use +N. Active compound filter chips wrap below the toolbar.
-
TABLE-004: Selection persists by stable IDs. All-matching selection requires a query key and eligible total, with explicit excluded IDs. Changing the query key invalidates stale all-selection. Bulk actions float 24px above the viewport bottom; three actions show directly and additional actions use overflow.
-
TABLE-005: Totals are supplied by column ID through footerConfig. The first visible column labels the total; Add line is a separate explicit application action.
-
TABLE-006: Column width handles support pointer capture, bounded drag, RTL, 10px arrow steps, Shift 50px, Home/End and Escape rollback. Widths and pinning can be controlled independently. Logical pin offsets include selection/action columns and follow resized or hidden columns.
-
TABLE-007: Group bars use a 2px inset, pearl-100 and radius 6px. Collapse preserves selection. Group only the current page; page counts and select-current-page refer to records, including collapsed descendants.
-
TABLE-008: Toolbar tools precede overflow and a divider; at most one secondary and one primary action follow. Export becomes an icon when actions are present. Primary plus alternatives uses SplitButton. Bulk actions never move into the ordinary toolbar.
-
FILTER-001: AND across fields, OR within multiple choices;
isNotnegates membership. Remote tables forward controlled values/operators to the application and never filter the received page. -
FILTER-002: Menus align to their trigger at 4px offset. Single choice replaces; multiple choices retain selection while searching and show selected values first. Date presets/ranges reuse DateCalendar. Numeric ranges require valid ascending endpoints. Clear resets only the owning field.
-
FILTER-003: Option A chips are bordered, radius 6px and minimum 28px. Field/value reopen the editor; operator opens only compatible operators; remove clears that field. Separate chips wrap beneath the toolbar.
-
FILTER-004: More filters adds a secondary field and opens its editor immediately. +N exposes filters that do not fit. No active field becomes inaccessible when the container narrows.
-
TABLE-009: Header master
430:13536/428:76: 36px high, Label/12 Strong, 12px icons, 4px gaps, pearl-800 at rest and pearl-1000 for interactive hover/focus. The neutral selector appears on unsorted hover/focus; sorted direction stays visible. End-aligned headers place sorting before the label. Optional information has an independent keyboard-focusable tooltip. Table backgrounds are white; DataGrid headers retain pearl-100 and vertical rules.
Shared cells
- CELL-001: Text starts at the logical leading edge; numbers align at the trailing edge and use tabular figures. Empty values show an en dash. Entity flags are 16px, person avatars 20px. Entity names wrap; do not truncate monetary amounts. Cells compose existing Badge, Tag, Avatar, ProgressBar and exact icon assets.
- CELL-002: Currency units remain quiet and separate from values. Fixed decimals are supplied via Intl format options. Trend direction and semantic intent are separate. A cost increase can be critical even though its arrow points up.
- CELL-003: At most primary bordered icon, secondary icon and overflow in a row action group. Do not repeat text CTAs in every row.
- CELL-EDIT-001: Compact text/number editor minimum 28px and radius 6px; calm border at rest, outline on hover/focus, persistent invalid treatment. Existing Select and DatePicker keep their accessible field geometry; the DatePicker retains the user's approved Kakashi DateField padding rather than shrinking its segmented inputs.
- CELL-EDIT-002: Enter or leaving the editor commits one cell; Escape cancels. Pending work cannot duplicate; errors retain drafts and offer retry. React portal focus stays within the same editing session. Application code validates and persists canonical values.
The Figma cell master depicts combinations, not 32 separate exported component classes. The shared content family composes those visual patterns. Toggle and upload actions use existing Switch/FileUpload application workflows in custom cells; they do not pretend to be string editors. TableCell content, TableNumberCell, TableRowActions and TableEditCell work in both owning components.
Explicit differences and boundaries
-
Selection: the Table prototype's row-click selection is superseded by its written best practice: row activation opens a record; checkbox selects. The implementation follows that distinction.
-
Accessibility: quiet text uses pearl-800 for contrast. Long content and text zoom can grow rows. Narrow viewports release horizontal pinning. Copy controls remain visible on touch, and long status labels expose their full text on focus/hover.
-
Application patterns: presets, summary aggregates, “Saved just now”, network requests, exports, permissions and financial calculations are owned by the app. Demos perform local updates or simulated requests and say so.
-
States and scale: matrix loading/error fallbacks reuse existing primitives. No virtualization, range operations or spreadsheet formulas are claimed. Stress fixtures do not establish an unlimited-data performance guarantee.
AI consumers should read this page together with the component API and integration pages. Do not infer a public prop from a Figma variant name, or treat a prototype's sample amounts as domain validation rules.
Header and cell master reconciliation
| Reference | Coverage |
|---|---|
| Header master 430:13536 | Start/center/end alignment, unsorted/ascending/descending, hover/focus, optional information, checkbox and empty action header; Headers, Selection and Pinning examples. |
| Groups and add row 445:13858 | Grouped filings show flags, counts, collapse and page-selection persistence; editable lines show totals and Add line. Application callbacks own actual persistence. |
Long content and zoom can grow rows beyond Figma's nominal heights. Linear progress and ring progress use their existing Pathfinder primitives; cell recipes do not introduce a new progress implementation. A file-picker example selects a local file and displays its name; production upload, progress and retry belong to the application's upload workflow. These are deliberate compositional boundaries, not built-in backend features.