Pathfinderby Commenda
Components

Data grid

Build financial matrices with grouped columns, expandable rows, totals and editable values.

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

Line itemQ1 2026
ActualBudget
RevenueUSD450,000USD420,000
Operating expensesUSD185,000USD190,000

Use DataGrid when readers compare values across periods or entities. Each record supplies a row label and values; the grid preserves that context while navigating cells. Use Table for searchable record lists with selection and pagination.

Usage

Example.tsx
'use client';

import { DataGrid } from '@commenda/pathfinder/data-grid';
import { TableNumberCell } from '@commenda/pathfinder/table';

Import @commenda/pathfinder/styles.css once in your application layout. Each example's Code tab contains its complete source with formatting and line numbers. Start with stable row IDs, a rowHeaderConfig, and value columns; add grouping or editing when needed.

Grouped headers

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

Line itemFY 2026Variance
Q1Total actual
ActualBudget
RevenueUSD125,400USD120,000USD125,400USD5,400
ExpensesUSD86,000USD84,000USD86,000USD2,000

FY 2026 contains Q1 Actual/Budget and a Total column. Variance sits outside the year. This intentionally mixes header depths: groups span their descendant leaves and shallower branches span the remaining levels.

A group has { id, header, columns }; a value column has an accessor or renderer. Groups recurse, but all IDs must be unique across the tree and every group must contain columns. Use a Total column per period group when the domain supports that aggregate.

Expandable rows and totals

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

AccountAmount
Operating expenses
SalariesUSD70,000.00
Office rentUSD16,000.00
Total operating expensesUSD86,000.00
Grand totalUSD86,000.00

Collapse Operating expenses. The parent now shows its supplied aggregate; expand it to reveal Salaries, Office rent and a subtotal below the children. The grand total remains independent of expansion.

Control expanded IDs with expansionConfig. getChildren supplies the hierarchy and getSubtotal supplies the subtotal record. The app owns every aggregate. Do not add parent aggregates and their children together, or assume averages and ratios are additive.

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

Line itemAmountChange
RevenueUSD125,400.50+12.0%
ExpensesUSD86,000.00+4.0%
Other incomeUSD––

Amounts keep two decimal places and a separate USD code. Missing income is an en dash. Both trends point up, but rising expenses are critical while rising revenue is positive: direction does not determine business meaning.

TableNumberCell formats at the display boundary. Use unit="USD" for a separate ISO code; avoid also asking Intl to add a currency symbol unless repetition is intended. style: 'percent' expects a fraction (0.12 → 12%). For whole percentage values use value={12} with suffix="%" instead. Accounting precision, rounding and conversion belong to the application.

Frozen headers and scrolling

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

AccountJanFebMarAprMayJun
Account 01USD1,250USD1,250USD1,250USD1,250USD1,250USD1,250
Account 02USD2,500USD2,500USD2,500USD2,500USD2,500USD2,500
Account 03USD3,750USD3,750USD3,750USD3,750USD3,750USD3,750
Account 04USD5,000USD5,000USD5,000USD5,000USD5,000USD5,000
Account 05USD6,250USD6,250USD6,250USD6,250USD6,250USD6,250
Account 06USD7,500USD7,500USD7,500USD7,500USD7,500USD7,500
Account 07USD8,750USD8,750USD8,750USD8,750USD8,750USD8,750
Account 08USD10,000USD10,000USD10,000USD10,000USD10,000USD10,000
Account 09USD11,250USD11,250USD11,250USD11,250USD11,250USD11,250
Account 10USD12,500USD12,500USD12,500USD12,500USD12,500USD12,500
Account 11USD13,750USD13,750USD13,750USD13,750USD13,750USD13,750
Account 12USD15,000USD15,000USD15,000USD15,000USD15,000USD15,000
Account 13USD16,250USD16,250USD16,250USD16,250USD16,250USD16,250
Account 14USD17,500USD17,500USD17,500USD17,500USD17,500USD17,500
Account 15USD18,750USD18,750USD18,750USD18,750USD18,750USD18,750
Account 16USD20,000USD20,000USD20,000USD20,000USD20,000USD20,000
Total budgetUSD170,000USD170,000USD170,000USD170,000USD170,000USD170,000

Scroll through sixteen accounts and six monthly columns. The first column preserves row context, headers stay above the values, and the grand total pins below them. Set a bounded scrollConfig.maxHeight rather than allowing a long matrix to dominate the page.

At narrow widths, horizontal pinning releases when the label plus one value column cannot fit. Values remain reachable through horizontal scrolling. The example repeats a monthly budget for clarity; real monthly amounts and totals should come from your reporting data.

Editing allocations

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

EntityITHRLegal
Meridian GmbH
Meridian US Inc.
Meridian UK Ltd.
Total100.00%100.00%100.00%

All changes saved

Click the German entity's Legal allocation, change 35 to 40, then press Enter. The example saves the value, updates “Saved just now”, and marks the Legal total 105% with an explanation. A successful save and a valid aggregate are separate states.

Only leaf record cells edit; parents, subtotals and totals remain read-only. Enter or leaving a changed text/number cell saves; Escape cancels. Finish or cancel an open editor before starting another. Select/calendar portals remain part of the same editing session.

The app owns persistence and the status line. After the request resolves, replace the row immutably with the canonical response. Use server versions or another concurrency policy to reject stale writes from other sessions.

Save failure and retry

Simulate a failed save

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

EntityAllocation
Acme GmbH

Changes are saved locally in this demo

Change the outlined allocation and press Enter with simulated failure enabled. The draft stays available. Disable the simulation and choose Retry save. Success is announced only after the simulated request resolves; the example makes no network calls.

EventResult
Click, Enter or F2 on an editable valueOpens one editor with its canonical string draft
Valid changed draft + Enter or leavingStarts one commit transaction
Escape before persistenceRestores the original value and cell focus
Validation failureKeeps the editor and explanation visible
Pending requestPrevents duplicate commits
Rejected requestRetains the draft and exposes Retry save
Resolved requestCloses the editor; app supplies updated data and status

Use outlined for persistent resting borders. Text and number editors use compact 28px sizing; existing Select/DatePicker controls retain their accessible dimensions. Numeric drafts remain strings until validated and parsed by the application.

Profit and loss

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

Line itemFY 2026FY 2025Change vs FY 2025
Q1Q2Q3Q4TotalTotal
Revenue
ServicesUSD350,000USD380,000USD410,000USD450,000USD1,590,000USD1,400,000+13.6%
LicencesUSD100,000USD100,000USD100,000USD110,000USD410,000USD400,000+2.5%
Total revenueUSD450,000USD480,000USD510,000USD560,000USD2,000,000USD1,800,000+11.1%
Operating expenses
SalariesUSD140,000USD150,000USD155,000USD165,000USD610,000USD550,000+10.9%
Office rentUSD30,000USD30,000USD30,000USD30,000USD120,000USD120,0000.0%
SoftwareUSD15,000USD15,000USD15,000USD15,000USD60,000USD60,0000.0%
Total operating expensesUSD185,000USD195,000USD200,000USD210,000USD790,000USD730,000+8.2%
USD95,000USD105,000USD115,000USD130,000USD445,000USD420,000+6.0%
Operating profitUSD170,000USD180,000USD195,000USD220,000USD765,000USD650,000+17.7%

This complete example combines year/quarter bands, expandable account groups, period totals, prior-year comparison and operating profit. It demonstrates composition after the smaller examples above. Subtotals and profit are supplied deliberately; the grid is not a formula engine.

Loading, empty and error

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

EntityAllocation

Switch between skeletons, no allocation data and a retryable failure. These reuse Pathfinder's existing state components. Keep the reporting scope stable during loading and retry; distinguish “no data in this period” from a failed request.

Keyboard navigation

KeyBehavior
TabEnters the active owning cell; leaves the grid when not editing
Arrow keysMoves among cells; horizontal direction follows RTL
Home / EndMoves to the row edges
Ctrl + Home / EndMoves to the first/last grid cell
Enter / F2Edits a value or activates the row-group control
EscapeCancels an edit and restores cell focus

Native controls retain their own keyboard interactions while editing. Avoid independently tabbable action clusters in every matrix cell; record-heavy actions usually belong in Table.

Scope and performance

The grid renders its scoped matrix without virtualization. Use bounded periods/entities and profile representative row and column counts. Storybook's 1,000-row adversarial fixture is a stress scenario, not an unlimited-data performance guarantee. Range selection, fill handle, bulk paste and undo remain outside the Figma v1 scope.

See the Figma source coverage and deliberate differences for provenance, dimensions and accessibility adjustments.

API reference

PropTypeBehavior
labelRequiredstringRequired accessible grid name
rowsRequiredreadonly TRow[]Root rows; app owns immutable updates
columnsRequiredreadonly DataGridColumn<TRow>[]Recursive bands or value columns
rowConfigRequiredObjectView fieldsStable IDs; optional child and subtotal accessors
rowHeaderConfigRequiredObjectView fieldsFrozen row axis; width defaults to 240px
expansionConfigObjectView fields · 2 shapesControlled/default expanded row IDs
totalConfigObjectView fieldsApp-supplied grand total pinned at the bottom
scrollConfigObjectView fieldsBounded scrolling; default maximum height 480px
stateConfigTabularStateConfigView fieldsSkeleton, retryable error and first-use state
dataMode"local"|"remote"Local filters matching leaf paths; remote renders supplied rows unchanged
searchConfigSelectionProps<string> & { getText?: | ((row: TRow) => string) | undefined; placeholder?: string; }Controlled or local search; defaults to row labels
filterConfigObjectView fieldsDefinitions, controlled values/callback, controlled or default operators
toolbarConfigTableToolbarConfigView fieldsPrimary, secondary, export and overflow actions
toolbarReactNodeApp-owned matrix controls above the grid
classNamestringRoot classes
rowConfigObject fields
FieldTypeRequirement
getId(row: TRow) => stringRequired
getChildren((row: TRow) => readonly TRow[])Optional
getSubtotal((row: TRow) => TRow | undefined)Optional
rowHeaderConfigObject fields
FieldTypeRequirement
headerstringRequired
getLabel(row: TRow) => stringRequired
cell((row: TRow) => ReactNode)Optional
widthnumberOptional
expansionConfigObject fields

Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.

Option 1

FieldTypeRequirement
valuereadonly string[]Required
onValueChange(value: readonly string[]) => voidRequired

Do not supply: defaultValue.

Option 2

FieldTypeRequirement
defaultValuereadonly string[]Optional
onValueChange((value: readonly string[]) => void)Optional

Do not supply: value.

totalConfigObject fields
FieldTypeRequirement
rowTRowRequired
labelstringRequired
scrollConfigObject fields
FieldTypeRequirement
maxHeightnumberOptional
stateConfigObject fields
FieldTypeRequirement
isLoadingbooleanOptional
errorObjectOptional
emptyObjectOptional
stateConfig.errorObject fields
FieldTypeRequirement
messagestringRequired
onRetry() => voidRequired
stateConfig.emptyObject fields
FieldTypeRequirement
titlestringRequired
descriptionstringOptional
actionObjectOptional
stateConfig.empty.actionObject fields
FieldTypeRequirement
idstringRequired
labelstringRequired
onAction() => voidRequired
disabledbooleanOptional
filterConfigObject fields
FieldTypeRequirement
filtersreadonly TableFilter<TRow>[]Required
operatorsObjectOptional
valueReadonly<Record<string, readonly string[]>>Required
onValueChange(value: Record<string, readonly string[]>) => voidRequired
filterConfig.operatorsObject fields

Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.

Option 1

FieldTypeRequirement
valueReadonly<Record<string, TableFilterOperator>>Required
onValueChange(value: Readonly<Record<string, TableFilterOperator>>) => voidRequired

Do not supply: defaultValue.

Option 2

FieldTypeRequirement
defaultValueReadonly<Record<string, TableFilterOperator>>Optional
onValueChange((value: Readonly<Record<string, TableFilterOperator>>) => void)Optional

Do not supply: value.

toolbarConfigObject fields
FieldTypeRequirement
actionsreadonly TableAction[]Optional

Prefer named actions. Legacy actions use first=primary, second=secondary, remainder=overflow.

primaryAction(TableAction & { menu?: DropdownMenu; icon?: ReactNode; })Optional
secondaryActionObjectOptional
onExport(() => void)Optional
moreMenuObjectOptional
toolbarConfig.secondaryActionObject fields
FieldTypeRequirement
idstringRequired
labelstringRequired
onAction() => voidRequired
disabledbooleanOptional
toolbarConfig.moreMenuObject fields
FieldTypeRequirement
groupsDropdownGroup[]Required

A column group is { id, header, columns }; groups can nest recursively. A value column is { id, header, value?, cell?, width?, align?, editConfig?, validationMessage? }. Default width is 128px and alignment is end. IDs must be unique across the column tree; groups cannot be empty. Row IDs must be unique throughout an acyclic row tree. Invalid recursive structures throw a clear configuration error.

Use cell with TableCell/TableNumberCell for entity flags, numeric/currency formatting and trends. Header bands span their descendant leaves; shorter branches span the remaining header levels. The first column and headers remain visible while scrolling horizontally and vertically. If the container cannot fit the row header plus a value column, horizontal pinning releases so values remain reachable; column headers still pin vertically. Keyboard arrows follow logical order in RTL.

Editing configuration

A value column's editConfig contains:

PropertyTypeBehavior
editorTableCellEditorText, numeric draft, Select or segmented date/calendar
outlinedbooleanKeeps resting editor outlines visible
getValue(row) => stringEditable canonical draft
canEdit(row) => booleanOptional app permission predicate
validate(value, row) => string or undefinedApp validation before saving
onCommit(row, value) => void or Promise<void>App persistence and row update

Only leaf record rows edit; groups, subtotals and totals stay read-only. One editor opens at a time. Leaving a changed cell saves; Enter saves text/numbers; Escape discards the draft. Pending saves cannot duplicate. Rejection retains the draft with a readable error and retry. While an editor is still open, finish or cancel it before editing another cell. Do not silently accept failed requests.

validationMessage(row) exposes invalid totals with an accessible explanation and error color. The allocation example calculates totals in its application layer; changing Legal from 35 to 40 produces a 105% invalid total, not a modal alert. Number strings are parsed by the app after validation. Financial precision, permissions, stale-write protection and server transactions remain application responsibilities.

Design rules

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

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.

Matrix grid

  • GRID-001: Pearl-100 headers, 36px bands, 40px minimum body rows, 0.5px vertical/horizontal gridlines. Freeze the first column and headers. Grouped columns recurse; row-header width defaults to 240px and value columns to 128px.
  • GRID-002: Collapsed groups display supplied aggregates. Expanded groups place supplied subtotals below children; subtotal background is pearl-100. Grand total pins to the bottom with a pearl-400 1px top border. No inferred formulas or double-counting.
  • GRID-003: One active cell with inset blue-700 outline and blue-200 ring. Arrow keys navigate; Enter/F2 edits or opens a group; Escape cancels. No range selection, fill handle, bulk paste, undo or virtualization in this version.
  • GRID-004: Only leaf record cells edit. Totals/aggregates remain read-only. Invalid totals explain the problem with error styling and a tooltip, not a modal. The app owns calculations, precision, constraints, permission checks and persistence.

Quiet secondary text uses pearl-800 where the design's lighter text would fail contrast at these small sizes. Narrow layouts release horizontal pinning when the frozen columns would hide readable values and scroll within their own container; financial values retain their width. Storybook's shared worst-case fixture covers long CJK/emoji names, large negative values, 1,000 paginated records, empty data, RTL and a 320px container.

Explicit differences and boundaries

ReferenceCoverage
Cell master 430:13537Text/description/icon/badge, people/groups/email, entities, files, tags, boolean, progress, copy/link, numbers/units/currency/trends, loading and totals compose TableCell, TableNumberCell and TableRowActions. Tree parent/child/grandchild behavior belongs to the recursive DataGrid examples. Hover and selection share the row tint in both densities.
  • 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

ReferenceCoverage
Header master 430:13536Start/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:13858Grouped 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.

Search and filters

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

EntityCountryAllocationDue date
Meridian UKGB12002026-10-20
Meridian USUS24002026-11-10
Meridian GermanyDE8002026-10-25

searchConfig, filterConfig and toolbarConfig share Table’s contracts. The grid reuses its single/multiple choice, searchable options, date presets/ranges, numeric operators, active chips, overflow and clear controls. Supply toolbarConfig for named primary/secondary/overflow/export actions; toolbar remains available for custom controls.

Local mode searches row labels by default; searchConfig.getText adds searchable fields. Filters combine with AND between fields. Matching leaves retain and expand their ancestor paths, including groups whose labels match the search. Clearing the search and filters restores the previous expansion state. Expansion controls are unavailable while showing filtered paths.

GRID-FILTER-001: Local filtering hides subtotals and grand totals because arbitrary cell values cannot safely be aggregated. Clear filters to restore complete totals. If the API computes filtered aggregates, pass those with dataMode="remote".

API-owned filtering

Simulated API response. Search and filter callbacks own the request state.

Use arrow keys to navigate cells. Enter edits or opens a row group. Escape cancels editing. Tab leaves the grid.

EntityAllocation
Meridian UK1200
Meridian US2400
Meridian Germany800

GRID-FILTER-002: In dataMode="remote", the component renders the rows supplied by the application without applying local filtering. Control query, values and operators with their callbacks, cancel stale requests, and supply stateConfig for loading, errors and retry. The example simulates a response in memory; it does not perform a network request.

On this page