Cells and utilities
Cell components, column helpers and the utility functions.
@workspace/active-grid 11.0.0Status unknowncontent differs from the recorded release 11.0.0baseline ce081dc3
The text below is the package's own API reference, included unchanged. The complete list of exports, generated from the source, is on All exports.
Cell renderers, system-column factories, export functions, auto-size
utilities, hooks, Context, Editing, and the
translation/locale typing.
LookupGrid#
Lookup presents a single-record picker in a controlled dialog.
Supply open, on, title, columns, on, t and locale.
Server mode is the default and requires fetch; mode="client" requires
data instead. Confirmation or a selectable row's double-click calls
on.
Optional inputs include grid, get, is,
column, server, description, search,
max, grid, initial and page. The capability
map is forwarded to the inner grid. The picker does not define a server query
or authorize the selected record.
Cell Components#
import { TextCell, BadgeCell, StatusCell, DateCell, LinkCell, SelectionCell, ExpanderCell,} from '@workspace/active-grid';Pre-built cell renderers for use in custom column cell render functions.
| Component | Description |
|---|---|
Text | Default text renderer with truncation and title tooltip. |
Badge | Categorical badge: type, category, department, source. One fixed presentation. |
Status | Status badge on a Grid, drawn with the --grid- tokens. |
Date | Locale-aware date display. Date-only by default; see below. |
Link | Link renderer: a native anchor, or the host's ui.; see below. |
Selection | Checkbox for row selection (used by get). |
Expander | Chevron toggle for row expanding (used by get). |
Calendar day versus instant#
Date carries a semantic prop with two values, and the choice is not
cosmetic โ it decides whether the shown day may move with the reader's time
zone.
semantic | Meaning | Behaviour |
|---|---|---|
'date- (default) | A calendar fact: birth, order, measurement or effective date | The same day in every time zone |
'datetime' | A real moment: created, an audit or execution timestamp | Direct DateCell uses runtime-local date-fns formatting; use the dateTime semantic column for presentation-zone formatting |
Date-only is the default because that is what this cell actually receives:
body- renders it for columns with meta., while a
column that genuinely shows a timestamp goes through the date column-type
preset, whose semantic declaration is formatted by the cell orchestration.
<DateCell value={contract.startDate} /> // calendar day<DateCell value={row.createdAt} semantic="datetime" /> // moment in timeThe same distinction lives in the column-type presets: type: 'date' is a
calendar day, type: 'date is an instant. Prefer those over a hand-written
meta..
Added 2026-08-12 (AUI-01). Until then the cell did new Date(value) for its
whole Date | string | number contract. For an ISO date-only string that is UTC
midnight, after which format() rendered the local calendar day: under
TZ=America/ 2026- showed as 08-. Measured, not
reasoned โ eight of the nine tests in date- went red as soon as
the zone shifted. The parsing now runs through date from
@workspace/, the one boundary the whole repo shares.
Status versus categorical#
The two badge cells are separate on purpose. Badge presents a value that
belongs to a set (a category, a type, a department); Status presents a
value that says how something is doing, on the five-tone axis
Grid (neutral, info, success, warning, danger). A host
maps each tone onto its own status colours through the
--grid- and --grid- tokens.
Badge has no variant or colour prop โ it had one until 9.0.0 and it was
status semantics expressed in an axis that carries none (default is the
theme's accent colour, not "good"). A custom cell that genuinely needs a
coloured categorical badge renders the host's own badge component directly,
so that choice is visibly outside the grid's badge contract.
// Declarative โ preferred. The column says what each value means; the grid// renders it. See api/columns.md ยง Status columns.meta: { filterOptions: [{label: t('status.active'), value: 'ACTIVE'}], statusTones: {ACTIVE: 'success', LAPSED: 'danger'},}
// Imperative โ for values that are not a stable key (derived or already// translated), where a valueโtone map would break per locale.cell: (info) => ( <StatusCell value={info.getValue() ? t('session.active') : t('session.expired')} tone={info.getValue() ? 'success' : 'neutral'} />),| Prop | Type | Description |
|---|---|---|
value | string | null | The text to display. Renders nothing when empty. |
tone | Grid | Required. The generic UI-tone; the caller maps to it. |
label | Record<string, string> | Optional valueโlabel resolution, as on Badge. |
class | string | Extra classes on the badge. |
Links: native anchor or the host's link#
Link renders an internal link as a plain <a href>, so the browser
navigates. A host with a client-side router passes its link component once,
through the nearest Grid:
// `hostLink`: your router's link component, e.g. the default export of next/link
<GridDefaults ui={{linkComponent: hostLink}}>{children}</GridDefaults>The component receives {href, class and nothing else
(Grid; the type of the component is Grid). A nearer
Grid replaces it for the grids below; a nearer one that sets other
ui fields keeps it. An external link (is) is always a native
anchor that opens in a new tab.
cell: (info) => ( <LinkCell value={info.getValue()} href={(id) => `/policies/${id}`} />),| Prop | Type | Description |
|---|---|---|
value | string | null | undefined | The link text, and the argument of an href function. |
href | string | ((value: string) => string) | The destination. |
is | boolean | New tab, rel="noopener noreferrer", never the host link. |
class | string | Extra classes on the anchor. |
System Column Factories#
import {getSelectionColumn, getExpanderColumn} from '@workspace/active-grid';| Function | Description |
|---|---|
get | Returns a column definition for row-selection checkboxes. |
get | Returns a column definition for row expand/collapse toggles. |
System columns have enable, enable, and no
header menu by default. They are excluded from export. Row-numbering is
available via the show prop on <Active rather than a
factory function.
ContextMenuBuilder#
import { type ContextMenuBuilder, type ContextMenuItem, createContextMenu,} from '@workspace/active-grid';type ContextMenuBuilder<TData> = ( row: TData | null, context?: ContextMenuContext,) => ContextMenuItem<TData>[];
interface ContextMenuContext { kind: 'range' | 'cell' | 'row'; // what the opened menu is about columnId?: string; // the column of the cell it opened on selection?: {rangeCount: number; cellCount: number}; // for a 'range' menu selectedRowCount: number; // rows selected in the grid}
type ContextMenuItem<TData> = | 'separator' | { label: string; action?: (row: TData) => void; // optional for submenu triggers icon?: React.ElementType; variant?: 'default' | 'destructive'; disabled?: boolean; disabledReason?: string; shortcut?: string; isLoading?: boolean; keepOpen?: boolean; permission?: string; // the RIGHT the viewer needs visible?: (row: TData) => boolean; // relevance for THIS row separatorBefore?: boolean; // explicit group boundary children?: ContextMenuItem<TData>[]; // nested submenu items };When the builder runs, and what it is told#
The grid calls the builder for a row that stands for a record, when that
row's menu opens, and passes the context as the second argument. A builder
that declares only row keeps working unchanged; the argument is optional so
a builder can still be called with a row alone. A row the grid generated (a
client group row) never reaches the builder: its menu is the grid's copy of
what its cells show.
kind is range when the menu opened inside a cell selection of more than
one cell (selection then counts every range together), cell when it opened
on a single cell, and row when there is no cell target (cell selection is
off, or the menu opened outside the row's cells). The grid's own copy items
follow the same kinds; the builder's items are the row's actions and stay in
every kind.
const contextMenu: ContextMenuBuilder<Person> = (row, context) => { if (!row) return []; return [ {label: 'View details', action: (r) => open(r.id)}, ...(context?.kind === 'range' ? [] : [{label: 'Edit', action: edit}]), ];};Permission, visibility and disabled are three questions#
They are answered separately and must not be substituted for one another:
| Field | Question | Result when it fails |
|---|---|---|
permission | May this VIEWER perform the action at all? | the item is absent |
visible | Is the action relevant for THIS row/state? | the item is absent |
disabled | Can it run right now, given the current state? | shown, with a reason |
An item carrying both permission and visible must pass both. Filtering
happens before separator normalization, so a group that filters away
completely leaves no orphaned rule behind.
Put a right in permission, never in visible: that is where the factory, the
docs and the next reader look for it. visible is for row and state predicates
((row) => row.). Both are client-side affordances -- the
server action behind the item authorizes independently.
A declared permission is never ignored. It is answered by the
has checker passed to create, and nothing else:
| Item | Checker answers yes | Checker answers no | No checker |
|---|---|---|---|
without permission | shown | shown | shown |
with permission, via the factory | shown | absent | absent |
with permission, direct builder | -- | -- | absent |
So a menu that declares rights passes the viewer's checker to the factory.
An item no checker answered is left out at the render boundary as well, and
outside production the grid reports it once per opening through the <Grid
sink (warn, source Active, data: {label, permission}):
a declared right without its check is a configuration error, and failing
closed keeps it from offering an action the viewer may not take.
Submenu support#
Items with a children array render as a submenu trigger with a chevron
(>). action is optional for submenu parent items โ the trigger itself
does not perform an action.
Permission filtering is recursive: create filters both parent
items and their children by has. If the parent is filtered out,
its children are not rendered.
const contextMenu = createContextMenu<Person>({ hasPermission, customItems: (row) => { if (!row) return []; return [ { label: 'View details', icon: Eye, action: (r) => router.push(`/records/${r.id}`), }, 'separator' as const, { label: 'Change status', icon: ArrowRightLeft, permission: 'record.update', children: transitions.map((status) => ({ label: `Status: ${status}`, action: (r: Person) => changeStatus(r, status), })), }, ]; },});The resolve pipeline#
Every row menu passes through one pipeline, in this order:
filter on
permissionandvisible, recursively into submenus;expand
separatorinto a rule in the sequence;Before give the destructive tail its rule, when the menu itself makes that unambiguous;
clean up: no leading rule, no trailing rule, never two in a row.
Filtering comes first because filtering is what strands rules. The pipeline is
idempotent and never reorders, merges or drops an action. It is the one
pipeline of the Active product family (ADR-058): it lives in
@workspace/, and Active Charts runs it over its
menus too; the grid supplies the permission check as its keep step.
Submenus and reasons. children nests to any depth, each level its own
submenu. A disabled item keeps its disabled as the pointer's title
and as aria- for assistive technology, on a submenu trigger as
well; it stays focusable, so a keyboard reader hears why it cannot run.
create runs it with the permission check, and the grid runs it
again at the render boundary on whatever a builder hands it. That second place
is what makes the contract hold product-wide: a menu passed directly as a
Context never meets the factory, but it does meet the renderer.
The renderer applies visible and the separator rules. It has no
has, so it keeps an item that declares permission only when the
factory approved it; the factory returns approved items as fresh copies and
never mutates or marks the consumer's own objects.
Consumers declare plain items and write no clean-up of their own. There is nothing to import.
Destructive actions sit in their own block#
A destructive action, or a group of related lifecycle actions ending in one, belongs at the bottom of the consumer block, separated from the ordinary object actions by a single rule. The rule underneath it is drawn by the grid, which always closes the consumer block before its own Copy / Export / Reset items:
ViewEditโโโโโโโโโDelete <- variant: 'destructive'โโโโโโโโโ <- drawn by the gridCopyExport view (10)Reset columnsYou usually declare nothing. The pipeline adds the rule when the menu ends in a destructive action and something precedes it. It deliberately does nothing when:
the menu does not end in a destructive action (there is no tail, and only the consumer knows the intended boundary -- use
separator);Before the destructive action is the first or only visible item (a rule above it would separate nothing);
a rule already opens that group.
A mixed lifecycle group stays whole. The tail starts at the FIRST destructive item of the last segment, not the last one, so a media block of delete, restore and delete-permanently is one group with one rule above it -- no rule falls between the restore item and the permanent delete, even though the restore item is not itself destructive.
Use separator for a group the pipeline cannot infer, for example one
that does not end the menu. It carries the same name and meaning as
Grid on the bulk bar: one vocabulary per
package. A doubled or leading rule is removed afterwards, so declaring it
defensively is harmless.
GridContextMenu#
import {GridContextMenu} from '@workspace/active-grid';Wrapper component for the Base UI right-click menu. Most consumers do
not import this directly โ the grid uses it internally when a context
prop is supplied.
EditingCommitPayload#
import {type EditingCommitPayload} from '@workspace/active-grid';Payload received by on:
| Property | Type | Description |
|---|---|---|
row | string | ID of the row being edited. |
original | TData | Row data before editing. |
changes | Record<string, unknown> | Only the changed fields. |
new | TData | Complete row data after edits. |
Related types: Batch, Dirty, Cell,
Cell, Edit โ for the batch/range-editing surface
(advanced).
Spreadsheet-style editing#
Combine edit with commit for spreadsheet-like
behavior:
<ActiveGrid<Row> gridId="editor" mode="client" data={rows} columns={columns} editMode="cell" editTrigger="click" commitOnBlur onDataCommit={async (payload) => { setRows((prev) => prev.map((row) => getRowId(row) === payload.rowId ? payload.newData : row, ), ); }}/>Behavior: single click starts editing; clicking another cell commits + moves; clicking outside the grid commits; Enter commits and moves down; Tab commits and moves right; Escape cancels.
Export Functions#
import { exportToCSV, exportToCSVGrouped, exportToCSVServerGrouped, exportToJSON, exportToJSONGrouped, exportToJSONServerGrouped, exportToExcel, exportToExcelGrouped, exportToExcelServerGrouped, exportToPDF, exportToPDFGrouped, exportToPDFServerGrouped, copyToClipboard, GridExportError, type GridExportErrorCode, type GridExportErrorOptions, type GridExportMode, type GridExportRunOptions, type GridExcelExportOptions, type GridPdfExportOptions, type GridExportFonts, type GridExportFontFamily, type GridExportFontFace, type GridExportFontSource, type GridExportDirection, type GridClipboardOptions, type ServerGroupedExportConfig,} from '@workspace/active-grid';| Function | Description |
|---|---|
export | Download CSV. Every field quoted, formulas neutralized. |
export | Download JSON file. |
export | Download Excel . with typed cells (optional xlsx, loaded on use). options. opens the sheet right to left. |
export | Download PDF (optional jspdf + jspdf-). Presentation-only; options. and options. (ADR-054). |
copy | Copy rows as TSV presentation copy. Formulas neutralized. A refused write resolves false and is reported to options. (default: the console). |
options on the CSV/JSON/XLSX functions is a Grid
(Grid union): omit it (or pass {mode: 'data'}) for the default
DATA export โ machine/raw semantics, byte-stable under every viewer
language โ or pass {mode: 'presentation', locale} for the explicit
human-readable form. export takes a Grid: the
same options plus direction (Grid, 'ltr' | 'rtl'). PDF
is presentation output by definition and takes a required
Grid ({locale, fonts?, direction?}); the clipboard is a
presentation copy and takes a required Grid
({locale, include). See
guides/ for the full mode contract.
Fonts are PDF-only, by type. Grid (families in fallback
order; each face source is bytes or a lazy host loader, with weight and
style) is accepted by Grid, the instance's
export. and Grid' export., and by nothing else: the
CSV, JSON and XLSX options have no font field. The shape is structurally the
one of active-charts' ChartExportFonts, so one host object serves both. See
guides/.
Errors. An export the grid refuses, or whose optional backend is missing,
fails with a Grid whose code is stable
(Grid: 'EXPORT_, 'EXPORT_,
'EXPORT_, 'EXPORT_,
'EXPORT_, 'EXPORT_,
'EXPORT_, 'EXPORT_) and whose metadata never
holds exported text. See
guides/.
Outcome. Every function reports by itself: a synchronous one throws, an
asynchronous one rejects, and a return means the file was handed to the
browser. An export from the row menu reports through
Grid and
on (Grid: 'page',
'loaded', 'selection'); see
guides/. No
export is cancellable.
Each export function has -Grouped and -Server variants for
client-side and server-side grouped data respectively. The
-Server variants accept a Server parameter.
Delimited-output rules (CSV + clipboard)#
All CSV and TSV rendering goes through one internal boundary
(src/ โ not part of the public API), so the rules
below hold for data cells, header rows, group labels and group footers alike:
CSV: every field is quoted, including headers; internal quotes doubled per RFC 4180; embedded newlines kept inside the quoted field.
TSV: minimal quoting โ only values containing a tab, newline or
"are quoted, so plain values paste cleanly into text targets.Formula neutralization: a value a spreadsheet would execute gets a leading apostrophe. Always on, no opt-out. The output of
formatand the percent formatters is deliberately untouched in every supported locale, so negative amounts export unchanged.Number
XLSX, JSON and PDF do not use this boundary. XLSX writes typed cells; a string cell is stored as text and is never evaluated as a formula.
Full behaviour, including the measured limits, is in
guides/.
Auto-size Utilities#
import {useAutoSize} from '@workspace/active-grid';| Hook | Description |
|---|---|
use | React hook that triggers auto-size on data-changes. Use with auto for first-render fit. |
The two standalone auto-size functions exported in v3 were removed in
v4.0.1 โ the use hook is the canonical entry point. See
CHANGELOG. v4.0.1.
import { type AutoSizeMode, type AutoSizeOnMountOptions, type AutoSizeOptions, type AutoSizeResult, type HeaderMeasureResult, type MeasureContext, DEFAULT_AUTO_SIZE_OPTIONS, DEFAULT_AUTO_SIZE_ON_MOUNT, getAutoSizeConfig, normalizeAutoSizeOnMount,} from '@workspace/active-grid';Three auto-size modes: 'fit-, 'fit-, 'balanced'.
Hooks#
import { useColumnSizingVars, useKeyboardNavigation, useAutoSize, useResponsiveColumns, type UseResponsiveColumnsOptions,} from '@workspace/active-grid';| Hook | Description |
|---|---|
use | Computes CSS-variable map for column widths from engine state. |
use | Arrow-key navigation + scroll-to-focus. get returns on (a primary press makes the cell active on the press) and on; spread both. |
use | Trigger auto-size on data-changes. |
use | Show/hide columns based on container width breakpoints. |
In most cases <Active handles these internally. They are exported
for advanced consumers composing grid behavior outside of <Active.
i18n Typing#
import { englishGridTranslator, useGridTranslation, useGridLocale, type GridTranslateFunction,} from '@workspace/active-grid';type GridTranslateFunction = ( key: string, variables?: Record<string, string | number | undefined>,) => string;Compatible with any i18n library. The translator comes from the grid's t
prop, else the nearest <Grid (see
columns. ยง GridDefaults); with neither, the grid
throws a configuration error. A translator is authoritative: its answer is the
text, including the raw key it returns for a key it does not carry, and the
grid never falls back per key (ADR-034, ADR-045).
english is the package's English translator and the key
contract of every grid text (src/). A
standalone host, a fixture or an example passes it on purpose
(t={english); the grid never installs it itself. A key
outside its table returns the raw key.
use and use return the EFFECTIVE translator
and presentation of the enclosing grid, including an instance override, for a
custom cell or editor that renders grid-owned words or formats a value. Outside
a grid both throw a configuration error; there is no default presentation
(active-i18n D14).
Utility Exports#
import { serializeDecimals, clearAllGridStates, resolveColumnBorderMode,} from '@workspace/active-grid';| Function | Description |
|---|---|
serialize | Convert Prisma Decimal instances to plain numbers/strings for client transit. |
clear | Remove all active_ keys from localStorage (e.g. on logout). |
resolve | Internal helper for show (rarely needed by consumers). |
Type Re-exports#
Grid objects and state slices#
The objects a consumer receives in renderers and callbacks, and the state slices it persists or controls, are declared by the package itself. They describe what a consumer may rely on and nothing about how the grid computes it.
import { type Grid, type Row, type Column, type Cell, type Header, type HeaderGroup, type CellContext, type HeaderContext, type SortingState, type ColumnFiltersState, type GroupingState, type SourceRow, type GeneratedRow, type SourceCell, type GeneratedCell, type GeneratedCellContext,} from '@workspace/active-grid';| Type | Carries |
|---|---|
Grid<TData> | row models, column and header accessors, state setters and per-slice resets |
Row<TData> | a Source or a Generated; get tells which and narrows |
Source | a row that stands for a record: identity, hierarchy, values, cells, state, original |
Generated | a row grouping generated: the same members, no record, grouping/grouping |
Column<TData, TValue> | definition, accessor, visibility, pinning, sizing, sorting, filtering, grouping, facets |
Cell<TData, TValue> | a Source or a Generated: the row and column it pairs, its value, predicates |
Header<TData, TValue>, Header | the header hierarchy, column spans, sizes and the resize handler; no row span |
Cell / Header | what a cell / header / footer renderer receives |
Generated | what an aggregated renderer receives: a generated row, which has no record |
A row a grid hands out stands for a record, or grouping generated it. Only a
source row carries a record, and the types say so: row. is TData on
a Source and undefined on a Generated. Code that can meet either
narrows first:
sortFn: (left, right) => left.getIsGrouped() || right.getIsGrouped() ? 0 : left.original.name.localeCompare(right.original.name, locale),Where only source rows arrive the record needs no narrowing: a filter function
receives a Source, an aggregation's leaf rows are Sources, and a body
cell renderer's Cell carries a Source. A comparator, an
eligibility predicate (enable), render,
get and a row model can hand over a generated row.
Column, Cell, Header and the two contexts carry the column's value type:
a renderer of a numeric column reads a number from get without an
annotation. Row. names a column by id, so the caller states
the type (row.).
A collection the grid hands out (get, get,
row., โฆ) is read-only: build a new array to arrange it
differently. Every slice reset takes an optional flag: reset restores
the initial state, reset the grid's own default.
row. returns every descendant of the row in preorder, interior
rows included, not only rows without children. Identifiers are not policed for
uniqueness: two records (or two columns) with the same identifier are both
presented, and a lookup by that identifier answers the last one. Supply unique
identifiers when uniqueness matters.
Row models and selection. Within one row model a logical row is one
object: the rows a model presents, its flat rows and its lookup hand back the
same object, and a flat row list holds each row once. Each selected-row model
projects the model it names: get holds only
selected rows the filter kept, get only rows the
grouped presentation carries, and get the selected
rows of the current page.
Pinning and positions. A pinned row or column that no longer exists is
ignored, so restored state may name rows that have gone. A pinned row the
reader folded away, inside a collapsed row or group, is not presented.
get answers -1 when not pinned and otherwise the zero-based
position within the pinned section, for a row and a column alike.
column. counts within one section: the column's own
without an argument, the named one with it, and -1 for a column not laid out
there. Offsets (column., column., header.)
are measured within the column's own section, because a pinned section does
not scroll with the centre. A header has column spans only: a shallower column
is carried up by placeholder headers.
Group rows and facets. A group row's id is its grouping path; when another
row already carries that id, the smallest free suffix #2, #3, โฆ is
appended, so a group row never shadows a source row.
column. counts present finite numbers only and
answers nothing when there are none. The grid has no global faceting:
get, get and
get throw instead of answering an empty result.
get, get and get answer the function the
grid actually applies. grid. for an id the grid does not carry
throws an error naming the id, in every environment.
Other re-exported types#
import { type GroupAggregation, type GroupByColumn, type GroupInfo, type ServerGroupRow, type ServerLoadMoreRow, isServerGroupRow, isServerLoadMoreRow, type CellPosition, type ColumnBorderMode, type ColumnFilterType, type FilterComponentProps, type FacetOption, type HeaderRendererProps, type GridExportFormat, type GridExportOptions, type GridExportScope, type GridMeta, type GridQueryRequest,} from '@workspace/active-grid';Editing types#
import { type EditMode, type EditingCommitPayload, type BatchCommitPayload, type DirtyCellEntry, type CellPosition, type CellRange, type CellSelectionState,} from '@workspace/active-grid';| Symbol | Type | Description |
|---|---|---|
Edit | 'cell' | 'row' | 'batch' | The three editing modes. Set via the edit prop on <Active. |
Editing | { row | Argument to on. Contains the diff and the merged row. |
Batch | { changes: Editing | Argument to on (batch-mode). All pending edits in one payload. |
Dirty | { row | One pending change in batch mode. Used by the internal batch-toolbar and dirty-tracking helpers. |
Cell | { row | Identifies a cell by numeric row and column indices. |
Cell | { start | A rectangular cell-range. Used by enable. |
Cell | { ranges: Cell | The grid's internal selection shape. No callback receives it: on gets Cell. |