Grid props
Every prop of ActiveGrid and GridDefaults, with its type and default.
@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.
<Active component, Grid discriminated union, all base
props, imperative handle, settings, pagination, toolbar, header/row menus,
and footer.
ActiveGrid Component#
import {ActiveGrid} from '@workspace/active-grid';The main grid component. Accepts Grid which is a discriminated
union based on the mode prop.
GridProps (Client Mode)#
<ActiveGrid mode="client" data={TData[]} // ... all common base-props (see section below)/>GridProps (Server Mode)#
<ActiveGrid mode="server" fetchFn={ServerFetchFunction<TData>} // ... all common base-props (see section below)/>Common base props#
All props shared between client and server mode. These are the
intersection of Grid-client and Grid-server. There is
no standalone type-export for this intersection; use Grid
and discriminate on the mode-field.
| Prop | Type | Default | Description |
|---|---|---|---|
grid | string | (required) | Unique identifier. Used for persistence keys and URL sync. |
columns | Grid | (required) | Column definitions array. |
height | number | 'auto' | 400 | Grid height. Number for fixed pixels with scroll; 'auto' for content-driven height. |
enable | boolean | false | Persist grid state (sorting, filters, sizing) to localStorage. Restored state is structurally validated — see State persistence. |
enable | boolean | false | Sync grid state to URL query parameters. The grid owns exactly five keys and leaves every other query parameter alone — see URL state. |
settings | Grid | undefined | Visual settings (row height, striping, footer styling). |
title | string | undefined | Grid title (displayed in toolbar). |
description | string | undefined | Grid description. |
search | string | 'Search...' | Placeholder text for the global search input. |
toolbar | Grid | undefined | Toolbar configuration (search, actions). |
pagination | Grid | {enabled: true} | Pagination configuration. |
header | Grid | {enabled: true} | Per-column header menu configuration. |
row | Grid | undefined | Row context menu configuration. |
export | boolean | Grid | true | Export configuration. true enables all formats. on / on report the outcome of a menu export (ADR-046). fonts (Grid) sets this grid's PDF export fonts over the Grid set (ADR-054). |
on | (row: TData) => void | undefined | Callback when a row is clicked. |
on | (row: TData) => void | undefined | Callback when a row is double-clicked. |
on | (row: TData, column | undefined | Fires on cell click with row data, column ID, and value. Both on and on fire on the same click (cell first via event bubbling). |
context | Context | undefined | Custom context menu builder: (row, context?) => items, called for a source row when its menu opens; context is a Context. |
class | string | undefined | Additional CSS class on the grid container. |
column | Grid | undefined | Default values applied to all columns. |
column | Grid | undefined | Extends or overrides the built-in column types per type name; the built-in types apply without it (ADR-045). |
initial | number | 10 | Page size a grid OPENS at when it has no pagination state of its own. Loses to an explicit URL or persisted value, wins over the package default of 10. See Initial page size. |
enable | boolean | true | Allow columns to be resized by dragging, and adds "Increase width" / "Decrease width" to the column menu as the keyboard path (see guides/). |
column | 'on | 'on | When to update column size — during drag or after release. |
enable | boolean | true | Allow columns to be pinned left/right. |
enable | boolean | false | Allow columns to be reordered via drag-and-drop, and adds "Move left" / "Move right" to the column menu as the keyboard path (see guides/). |
enable | boolean | true | Arrow-key navigation between cells, and Enter to edit an editable cell. On by default since 8.0.0: role="grid" without it is an incorrect role declaration. Pass false to opt out. |
on | (row: TData, column | undefined | Fires when Enter/Space activates a focused cell. Not fired for editable cells — there Enter starts the edit instead. |
default | number | 120 | Default minimum column width in pixels. Overrides global columns. for this grid. |
min | number | undefined | Minimum total grid width in pixels. Flex columns are distributed over at least this width; container never shrinks below it. When columns exceed this width (e.g. after auto-size), container grows — min is a floor, not a cap. |
enable | boolean | false | Allow rows to be pinned to top/bottom. |
is | (row: TData) => boolean | undefined | Predicate to control which rows can be pinned. |
max | number | 10 | Maximum number of pinned rows. |
get | (row: TData) => string | Auto-detect id field | Custom row-ID resolver. Falls back to row.. Required at the type level when TData has a closed key set without an id field. Must return a non-empty string; any other answer is refused with an error naming the record's position. Rows the grid generates itself (server-grouping headers, load-more rows) are named by the grid and never passed to it. |
enable | boolean | false | Allow rows to be expanded to show detail panels. |
render | (row: Row<TData>) => React | undefined | Render function for the expanded detail panel. |
get | (row: Row<TData>) => boolean | undefined | Predicate to control which rows can be expanded. |
show | boolean | false | Show auto-incrementing row numbers as the first column. |
enable | boolean | false | Enable row grouping. Columns with enable (default) can be grouped via header menu. Shows Grouping with removable chips when grouping is active, directly above the role="grid" element as the top of the grid's card. |
initial | Grouping (string[]) | [] | Initial grouping columns. Applied once on mount. |
on | (grouping: Grouping | undefined | Callback fired when grouping state changes. |
edit | 'cell' | 'row' | 'batch' | 'cell' | Editing mode — single cell, entire row, or buffered batch. Pairs with a commit callback: 'cell'/'row' → on, 'batch' → on. Without the matching callback editing is disabled entirely (fail-closed) with a dev-warn. |
edit | 'click' | 'double | 'double | How editing is triggered. 'click' starts editing on single click (spreadsheet-style). |
commit | boolean | false | When true, current edit is committed when focus leaves the cell. Useful for spreadsheet-style editing with edit. Moving to another editable cell commits the open edit first and opens the next once that commit is accepted, unless another edit began meanwhile; a refused value keeps the open edit. See ../ § Auto-cancel. |
on | (payload: Editing | undefined | Callback when edited data is committed. Required for edit / "row" — without it those modes offer no editing. |
on | (payload: Batch | undefined | Callback for "Save all" in batch mode. Required for edit — without it batch mode offers no editing. Unreachable in the other modes. The grid stays editable while it runs; its completion settles only the cells it received, and edits made meanwhile stay dirty for the next save (see the editing guide, "Editing while Save all runs"). Reject to report failure: nothing is settled. |
auto | boolean | Partial<Auto | false | Auto-size columns when grid mounts and data is available. |
t | Grid | inherited | This grid's translator override; else the nearest <Grid. Authoritative: a key it does not carry renders as that raw key and is never replaced by English (ADR-034). With neither, the grid throws a configuration error; english is the package English, passed on purpose (ADR-045). |
locale | Grid | inherited | This grid's presentation override; else the nearest <Grid, replaced whole. A locale code (notation follows the language), or a presentation-context object ({language) carrying language and notation independently. Relative date presets, the date-filter calendar and date filters need time and throw a configuration error without it (guides/ § Presentation and time zone). Forwarded untouched to the central @workspace/ formatters; the grid never resolves tenant or application settings itself. With neither, the grid throws a configuration error (ADR-034, ADR-045). Object inputs must be referentially stable across renders. |
dir | 'ltr' | 'rtl' | inherited | The grid's UI direction: the reading order of its chrome, controls and columns, and the direction every popup it opens carries (ADR-052). Left out, the grid inherits the page's direction (the nearest dir attribute, else the computed CSS direction) and follows a later change of that attribute. Never inferred from locale: set dir on a container for locale-driven direction. Any other value throws Grid (GRID_DIRECTION_INVALID). Under rtl the grid is mirrored (ADR-053): layout, pinning (logical start/end), scrolling, column virtualization, resize, reorder, keyboard (ArrowLeft/Right visual, Home/End logical), selection, menus, filters and the calendar; data and aria- order are unchanged. Not a text direction; see meta.. |
meta | Partial<Grid | undefined | Custom metadata passed to the grid instance. Used internally for editing-state, pinning-state, copy-callbacks (on, on), aggregations, facets, and more. |
virtualization | boolean | undefined | Controls row virtualization. true forces on, false forces off, omit for auto-detect. |
virtualization | number | 1000 | Row count above which virtualization is enabled (when virtualization is not set and height is a number). |
enable | boolean or (row: Row<TData>) => boolean | false | Enable selection and its checkbox column; a predicate receives a source/generated row and must narrow before reading original. |
on | (rows: TData[]) => void | undefined | Callback with selected row data on every selection change. |
grid | Ref<Grid | undefined | Imperative handle. Production consumer code should use use instead of calling its methods directly — see server-mode.md and ADR-009. |
restore | React. | undefined | Ref that receives a restoreFocus callback. Call after closing a modal/dialog to return focus to the last focused cell. Returns true if focus was restored, false if no cell was tracked. |
server | string | undefined | Opaque identity of the EXTERNAL query — the part of the question the grid does not own (a page toolbar search box, a folder picker, a URL-carried kind filter). Server mode only. When it changes the grid treats the result set as new: cache invalidated, refetch, page back to 0, row selection cleared. Build it from the values themselves so returning to a previous scope is genuinely the same key. Leave it undefined when the grid owns the whole question — the dataset key is then byte-identical to what it was before this prop existed. See server-mode.md. |
server | Server | undefined | Server-mode options: page caching, prefetch, block size. Only applies when mode="server". See server-mode.md. |
enable | boolean | true | Allow multiple row selection. When false, only single-select. With multiple row selection the grid declares aria-; see ../ § ARIA Roles. |
on | (model: Selection | undefined | Structural selection callback. Receives Selection discriminated union (inclusive / exclusive). See ../. |
selection | Readonly | undefined | Declarative bulk actions for the current selection. A non-empty array plus enable makes the grid draw its selection action bar ([N selected] [Actions v]) above the body and suppress the footer's selection counter. The shape does not change with the number of actions. See GridSelectionAction and ../. |
on | (sorting: Sorting | undefined | Callback fired when sort state changes. Receives the new sort spec. |
on | (filters: Column | undefined | Callback fired when column-filter state changes. |
enable | boolean | false | Enable horizontal (column) virtualization for very wide grids (50+ columns). Only the visible centre columns plus overscan are mounted; pinned columns are always mounted and aria- / aria- keep reporting the full, absolute column set. Explicit opt-in only — there is no threshold and no auto-detection, unlike row virtualization. Requires a numeric height; with height="auto" the grid dev-warns and does not clamp. Autosize still measures real cell content: the grid suspends the window for the duration of a measurement pass. Not supported under RTL. See ADR-030. |
loading | Loading | see below | What the grid shows while a fetch is in flight. Nothing on first load; the 2-pixel bar on a refetch. See Loading display. |
external | (row: TData) => boolean | undefined | Consumer-supplied row-predicate applied alongside column filters. Client mode only — in server mode the predicate is never called and the grid dev-warns once; see Filter axis: client mode only. |
post | (rows: TData[]) => TData[] | undefined | Re-ranks the sorted top-level rows. Receives their records in sorted order and returns them in the wanted order; the result is a ranking, not a row list: an omitted record keeps rank 0, a repeated record takes its last position, an unknown record adds nothing, and ties keep the sorted order. Under grouping each group is represented by its first member record in filtered order. Only the top-level rows move: each keeps its sub-rows in their order, and the flat row list follows the new order. Must be a pure function of its input: the grid calls it again when the sorted rows, the grouping or the function itself change, never merely because rows are read, so a changed ranking needs a new function. Not applied when sorting is manual. |
enable | boolean | false | Render a subtotal/footer-row at the end of each group (client-side grouping only). |
enable | boolean | false | Enable server-side grouping mode. Server returns is and groups metadata; the outer grouping hook constructs synthetic display rows. See ../. |
server | Group | undefined | Ordered {field, sort?} declarations passed to fetch as params.. |
enable | boolean | false | Hide columns automatically based on viewport breakpoints. Use with responsive to declare hide-thresholds per column. |
responsive | Record<string, number> | undefined | Column-id → minimum viewport width (px) below which the column is hidden. |
enable | boolean | false | Cell-range selection: drag, Shift+click and Shift+arrow extend from the active cell (the anchor), Ctrl/Cmd+click adds, Ctrl/Cmd+Shift+click replaces the last range; a click selects the active cell and stores no range. Ctrl+C and the context menu copy it. Also makes the body context menu range-aware. No paste or fill. Turning it off ends every range (reported as []); turning it on again brings none back. See ../ § Cell selection. |
on | (ranges: Cell | undefined | Called with every explicit range after each finished change (once at the end of a drag), and with [] when the selection ends. A click changes the active cell, not the ranges, and reports nothing; a single selected cell is []. |
enable | boolean | false | Show the AND/OR filter-builder panel UI for composite filter expressions. Client mode only — in server mode the button and panel are not rendered and the grid dev-warns once. See ../. |
initial | Filter | undefined | Initial filter-tree (AND/OR composition) for the filter-builder. Client mode only. Consumer-owned: the grid does not persist the expression itself. |
on | (expr: Filter | undefined | Callback when the filter-builder tree changes. Never fires in server mode, where no expression can be composed. |
enable | boolean | true with on, else false | Enable Ctrl+Z / Ctrl+Y undo/redo for cell edits (not in edit). Edit-history lives in the mounted grid and is dropped on unmount. See ../ § Undo/redo. |
max | number | 50 | Maximum number of undoable edits retained in history. Older edits are discarded. |
Loading display#
Why the default is nothing#
A grid has three states, and they are not interchangeable: it has not loaded yet, it has loaded and found nothing, or it has loaded and has rows. Two of those look identical if you only count rows, which is how a grid ends up saying "No results" while it is still fetching.
Two things follow, and they are of different kinds.
Whether an indicator is shown is your decision, not the library's. Only
you know whether your data arrives in 50 ms or in 3 seconds. An indicator that
appears and disappears within 80 ms is more disturbing than an empty area, and
a library that guesses will be wrong for half its consumers. So the default for
a first load is 'none'.
Whether the grid states something untrue is not your decision. Regardless
of loading, the pagination footer stays silent — no row count, no page
indicator — until a fetch has answered. That is correctness and it is not
configurable. With 'none' as the default it matters more, not less: it is the
only thing between an empty area and a false statement.
A refetch is the one place the library still leans in. Rows are on screen and
they have just gone stale; a reader given no cue is looking at old data without
knowing it. The default there is the 2-pixel bar. Set refetch: 'none' if you
disagree.
Type#
type LoadingIndicator = 'none' | 'progress' | 'skeleton' | 'spinner' | 'text';
// An indicator name, or your own node rendered inside the grid box.type LoadingDisplayValue = LoadingIndicator | React.ReactNode;
interface LoadingDisplayOptions { firstLoad?: LoadingDisplayValue; // default 'none' refetch?: LoadingDisplayValue; // default 'progress' delayMs?: number; // default 0}
loadingDisplay?: LoadingIndicator | LoadingDisplayOptions;All three are exported, so a wrapper can name them:
import type { LoadingDisplayOptions, LoadingDisplayValue, LoadingIndicator,} from '@workspace/active-grid';
// A per-screen choice passed down to a shared grid wrapper.function AppGrid({busyState}: {busyState: LoadingDisplayValue}) { … }Indicators#
| Value | What you see | When it fits |
|---|---|---|
'none' | Nothing. | Data arrives fast, or you render your own indicator outside the grid. |
'progress' | 2-pixel indeterminate bar along the top edge. Covers nothing; rows stay visible and interactive. | Slow queries where the rows must stay readable. The refetch default. |
'skeleton' | Placeholder rows matching the column widths, over an opaque background. Row count follows page. | First loads over ~1 s, where the shape of the table is already meaningful. |
'spinner' | Centered spinner over an opaque background. | Small grids whose column shape says nothing. |
'text' | One non-blocking line at the top edge ("Loading…" / "Refreshing data…"). | Dense screens where a skeleton is too loud but silence is confusing. |
React | Whatever you pass. | You have your own loader and want it here. |
Two details worth knowing. 'skeleton' renders nothing when the column
widths are not resolved yet — it does not silently become a spinner. And the
bar and the skeleton stop animating under prefers-; they stay
visible, they just hold still.
Why 'skeleton' stays#
'skeleton' is an explicit library capability. Host applications decide which
loading presentations their own design standards permit; supporting a value
does not require a host to use it. The repository's
PAGE_ section 7
restricts product loading UI while allowing this library capability.
Nothing about the default behaviour depends on this. first defaults to
'none' and refetch to 'progress', so a consumer that says nothing never
sees a skeleton — it has to be asked for by name.
Every way to write it#
// Default: nothing on first load, the bar on a refetch.<ActiveGrid mode="server" fetchFn={fetchFn} columns={columns} gridId="a" />
// Short form — symmetric. This is skeleton in BOTH states.<ActiveGrid loadingDisplay="skeleton" ... />
// Per state.<ActiveGrid loadingDisplay={{firstLoad: 'skeleton', refetch: 'progress'}} ... />
// Only override one; the other keeps its default.<ActiveGrid loadingDisplay={{firstLoad: 'text'}} ... />
// Hold anything back until the fetch has run 300 ms. A faster fetch// shows nothing at all — this is what removes the flash.<ActiveGrid loadingDisplay={{firstLoad: 'skeleton', delayMs: 300}} ... />
// Your own node — any ReactNode. The grid box is the positioning context.<ActiveGrid loadingDisplay={{firstLoad: <div>Loading orders…</div>, refetch: 'none'}} ... />
// Silence everywhere.<ActiveGrid loadingDisplay="none" ... />delay is recommended alongside every visible indicator; 200-300 ms is a
common choice. It is not defaulted to a non-zero value on purpose — that would
be the library deciding again.
Filter axis: client mode only#
external and enable are declared on the shared base-props,
so the type-checker accepts them in both modes — but both are applied in exactly
one place, and that place is client-only. use- filters the client
dataset under mode === 'client'; the filter-builder's AND/OR expression is
evaluated into that same predicate. Nothing is serialised onto the wire:
Server carries filters, sorting and global, and has no
expression field.
The two get different treatment in server mode, because they fail differently:
| Prop | Server-mode behaviour | Why |
|---|---|---|
external | Ignored, dev-warns once per grid | There is no UI to withhold — the consumer wrote a predicate in their own code. Applying it to the loaded page instead would filter one page of a server dataset and make the row counts lie. |
enable | Toolbar button and panel are not rendered; dev-warns once per grid | A user, not a developer, composes the expression. A console warning never reaches them, so the affordance itself is withheld rather than left over a discarded result. |
Both warnings are development-only (silent in production) and fire only when the prop is actually set — an ordinary server-mode grid is never warned about.
Consequence worth stating explicitly. When external is used to hide
rows (a view-tab predicate, a scope restriction), a server-mode grid shows the
user more than the consumer intended. That is a visibility gap, not a data leak:
permission and tenant scoping are enforced server-side by the resource layer and
never depend on this predicate. Still, move the condition into the server query
(fetch params, or a resource filter) rather than relying on the predicate.
Roadmap item F- covers wiring the expression tree into the server query. This
is "not yet", not "never".
Initial page size#
initial is the page size a grid opens at when it has no pagination
state of its own. It is a starting value, not an override: it never replaces
what a user chose or shared.
The initial… prefix is deliberate. A grid starting value is one link in a
precedence chain with URL and stored state (Grid), so it is not
the plain React initial value of uncontrolled state that a default… prop names
elsewhere in the package family (ADR-045 § 8). Callback names (on…) are the
same in both packages.
Precedence#
From strongest to weakest:
| # | Source | Wins when |
|---|---|---|
| 1 | An explicit, valid pagination state in the URL (enable) | The query string carries page and/or page |
| 2 | An explicit, valid persisted state (enable) | local holds a structurally valid slice |
| 3 | initial on this grid | Neither state source says anything |
| 4 | pagination. from the nearest Grid | The prop is absent or invalid |
| 5 | The package default of 10 | Nothing above says anything usable |
Sources 1 and 2 are ordered against each other by state (default
'url-) — see Precedence against localStorage.
That arbitration is unchanged; the prop and the boundary sit strictly below
both.
Steps 3, 4 and 5 are the same shape every other Grid setting has: a
per-grid prop wins over the nearest Grid, and that boundary wins over
the package default. See columns. § GridDefaults.
Consequences worth stating:
pagealways starts atIndex 0. The prop names a size, never a page.The first render already carries the size. The value seeds the store rather than arriving from an effect, so there is no visible jump from 10 to the configured size. Server and client agree on it — it comes from a prop, not from
localor the URL — so it is also hydration-safe.Storage A later render with a different value does not reset a running grid. The starting state is decided once, at mount.
A stale persisted or URL value still wins. A user who once chose 100 keeps 100, even when the code later declares
initial. Clearing the stored state (Page Size={20} clear) brings the grid back toAll Grid States() initial, not to 10.Page Size Two grids are independent. Each
gridhas its own store and its own starting size. Two grids under the same provider share the baseline unless one of them setsId initial.Page Size A provider re-render does not reset a running grid. The starting state is decided once, at mount, from both sources. To see a new configured value, remount the grid.
Invalid values#
A value that is present but not a whole number greater than zero (0, a
negative, a fraction, Na) is refused, not repaired. It is not clamped to
1 and not rounded — either would turn a typo into a working grid that silently
disagrees with its source. A console. in development names the grid and
the source that was rejected.
A refused value counts as absent, so the chain simply continues: an invalid
initial falls through to Grid, and an
invalid provider value falls through to the package default. The other reading —
a typo in one grid discards the deliberately configured platform value — would
punish the installation for a mistake somewhere else.
The value is deliberately not checked against page. That list
validates the URL, which is user input and an abuse surface (page);
initial comes from the developer composing the grid, and the paginator
already appends a size outside the list to its selector.
Fixed in 10.1.3. Before that release the prop only reached the state through the pagination branch of state hydration, which runs only when the URL or
localalready carries a pagination slice. On a fresh grid — exactly what "initial" names — it did nothing.Storage Extended in 10.2.0.
Gridwas declared, defaulted and documented — with a provider example that invited consumers to set it — and read by nothing. It is now step 4 of the chain above. A consumer who already set it and saw no effect will now get the value they asked for.Config. pagination. page Size
State persistence#
With enable, grid state is written to local under
active_. Two further namespaces exist for auxiliary state:
active_ (locked columns) and
active_ (pinned rows).
What is validated on load#
A stored entry is only trusted after two checks:
Version. A
versionthat does not match the configured storage version is discarded and the key is removed.Shape, per slice. Every slice is checked against the structure it is supposed to have —
sortingan array of{id: string, desc: boolean},columna record of finite numbers,Sizing paginationan object with a non-negativepageand a positiveIndex page, and so on. The same applies to the two auxiliary namespaces.Size
A version check alone is not enough: state can be corrupt within a valid
version, for instance after a hand-edit of local or a restore from
another source.
What happens to an invalid slice#
The slice is ignored and falls back to its default. It is never repaired or converted — there is no migration mechanism.
Validation is per slice, not all-or-nothing: the remaining slices are
restored normally. A single corrupt column therefore does not cost you
your column widths or your saved filters. This is a robustness boundary for UI
preferences, not a security boundary, so discarding state the user could have
kept is the worse outcome.
A completely unreadable entry (not JSON, or JSON that is not an object) yields no state at all and never throws.
The warning#
Each ignored slice logs a console. naming the slice, the grid ID and the
kind of value that was rejected:
[active-grid] ignored persisted "pagination" for grid "audit-log" — the storedvalue does not match the expected shape (received an array of 2). The slicefalls back to its default; the other slices were restored.This warning appears in development only (process.). The slice is dropped in production too — only the message is
suppressed. State that disappears without a signal is the hardest kind to
explain, which is why the drop is announced at all.
What is written#
A save writes exactly the slices a load restores: sorting, column,
column, column, column, column,
pagination, global, row and expanded, plus the storage
version and a timestamp. Runtime state is never written: row selection,
grouping, the live resize bookkeeping and cell selection belong to one visit.
An older entry that still carries such fields loads without complaint; they
are not restored, and the next save leaves them out.
A column width the reader set by dragging is a lock in
active_. The locks are read before the grid's first
layout pass, and no layout pass runs before they are known, so a restored
width survives the reload and every later resize of the viewport.
URL state#
With enable, the grid mirrors part of its state into the query string
so a view can be shared, bookmarked and reloaded.
What the grid owns#
Exactly five keys, and this list is exhaustive:
| Key | Grid state | Shape |
|---|---|---|
page | pagination. | 1-indexed (page=3 is page) |
page | pagination. | omitted when it equals the package default of 10 |
sort | sorting | name:asc,age:desc |
filters | column | salary:greater |
search | global | URL-encoded term |
page and page are read strictly: only decimal digits state a number, and
both must be a whole number greater than zero. page=2junk, page=1.5,
page=-1, page=0 or an empty value states no page, so the grid keeps its
starting page instead of a truncated or clamped one; page follows the same
rule.
No other grid state reaches the URL. Column sizing, ordering, pinning,
visibility, row pinning and client expansion are localStorage-only, under
enable. Server-group expansion is separate, non-persisted hook
state (see Server grouping) — they are per-device preferences, not part of a view you
share with a colleague.
What the grid does NOT touch#
A grid manages only its own URL state. Every query parameter outside the
five keys above belongs to the application, and the grid carries it through
untouched: same value, same multiplicity (two ?tag= pairs stay two pairs),
same relative order. It is never rewritten, re-encoded or dropped.
This matters for composition. A route can carry a tab id under ?section=, a
server-evaluated view under ?view=, or any other app-owned deep-link
dimension, and put a URL-synced grid on the same page:
/records?view=recent&page=2&sort=createdAt:descSorting, paging, filtering, searching and clearing the grid state all leave
view=recent exactly where it is.
Fixed in the release after 9.0.0. The write-back used to replace the whole query string, so an app-owned parameter disappeared on the first grid interaction — a deep link that worked once and then quietly did not. Consumers whose routes carry no parameters besides grid state see no behavioural change at all: for them the merge produces the same string the replacement did.
Not promised: byte-exact reproduction of the whole URL. The grid re-serializes its own keys from state, so their order and encoding come from the serializer and they are appended after the application's parameters.
History semantics#
The write-back uses history., never push: a sort is not a
navigation, and back/forward keep meaning what they meant before the grid state
changed. This is unchanged behaviour.
Two grids on one route#
The five keys are global to the route, not namespaced per grid. Two
URL-synced grids on the same route therefore compete for them and the last
writer wins. Give at most one grid per route enable; a second grid on
the same route uses enable only. (The grid does not warn about
this — it cannot see its siblings.)
Precedence against localStorage#
When both enable and enable are on, the four contested
slices (sorting, filters, global filter, pagination) resolve per
state, default 'url-. A shared link therefore wins over the
recipient's own saved state, which is what makes it a link worth sharing.
GridImperativeHandle#
import {type GridImperativeHandle} from '@workspace/active-grid';| Method | Description |
|---|---|
refetch() | Invalidate server-side cache and re-fetch current page. No-op in client mode (emits dev-warning). |
reset | Clear all row selections. |
add | Insert a row into the local cache. Updates total. No-op in client mode (emits dev-warning). |
remove | Remove a row from the local cache. Decrements total. No-op in client mode (emits dev-warning). |
Production consumer-code must use
useinstead of calling these methods directly. SeeGrid Mutation server-for the hook and the three exception classes (library tests, demo pages, pure UI commands outside a mutation flow).mode. md
SelectionModel#
import {type SelectionModel} from '@workspace/active-grid';Discriminated union for row-selection state:
type SelectionModel = | {mode: 'inclusive'; rowIds: string[]} | { mode: 'exclusive'; excludedRowIds: string[]; totalCount: number; query: SelectionQuery; };Inclusive mode (default) tracks selected rows by ID. Exclusive mode (used for server-side "select all 5000 rows") tracks excluded rows; everything else is considered selected.
total is for display. It is what the last response reported. A
mutation must never compute its own effect from it: rows can appear or
disappear between the selection and the call, and the number arrives from the
client anyway.
SelectionQuery#
import { type SelectionQuery, type SelectionQueryFilter,} from '@workspace/active-grid';The QUESTION an exclusive selection answers. "Everything except these ids" is meaningless without it: a consumer that cannot see the query either processes the rows it happens to have loaded (silently wrong) or the whole table (dangerously wrong).
interface SelectionQueryFilter { readonly id: string; readonly value: unknown;}
interface SelectionQuery { /** Column filters the grid itself owned at selection time. */ readonly filters: ReadonlyArray<SelectionQueryFilter>; /** The grid's own search term; `''` when the grid has no search. */ readonly search: string; /** The consumer's `serverQueryKey`, verbatim; `undefined` when unused. */ readonly externalKey: string | undefined;}Split by OWNER: filters and search are the grid's, external is the
consumer's. The grid never parses any of it — it is carried, not interpreted,
so a consumer can verify the selection still belongs to the scope on screen.
A mutation must still re-derive and re-authorize all of it server-side; see
../.
GridSelectionAction#
import { type GridSelectionAction, type GridSelectionActionTone,} from '@workspace/active-grid';One action offered on the current selection. The grid owns the bar, the count, the fixed Actions menu, the keyboard behaviour and the two presentations; the consumer owns which actions exist, what they are called and what they do.
| Property | Type | Default | Description |
|---|---|---|---|
id | string | — | Stable identity. Never a translated label. |
label | string | — | Visible text, already translated by the consumer. |
icon | React. | undefined | Optional leading icon component. |
tone | Grid ('default' | 'danger') | 'default' | danger renders the destructive menu-item presentation. |
separator | boolean | false | Draw a separator above this item. |
supports | boolean | true | Whether the action can execute an exclusive selection. See below. |
on | (selection: Selection | — | Receives the current model unchanged. An async handler holds the whole menu busy until it settles. |
Permission is absence. An action the caller may not perform is left out of the array; the grid never renders a disabled item to stand for a missing right.
There is no React slot. A free slot would let every consumer design
its own bulk bar again, which is the divergence this contract replaces. One
action renders in the same menu as five.
supports closes the affordance gap. A grid that
offers "select all 5000 rows" next to an action that can only process explicit
ids invites the reader into a state where the action either does nothing or
silently does less than it says. The declaration works in both directions:
an action that cannot execute the exclusive shape is left OUT of the menu while such a selection is active;
when NO declared action can execute it, the grid does not offer dataset-wide select-all in the first place.
Set it to false when the action needs explicit ids — because its server side
takes an id list, or because the work is unbounded and has no background
carrier. Leave it true when the server can re-derive the query, re-authorize
it and apply the exclusions itself.
GridSettings#
import {type GridSettings} from '@workspace/active-grid';Visual settings via the settings prop:
| Property | Type | Default | Description |
|---|---|---|---|
row | number | 42 | Row height in pixels. |
header | number | 48 | Header height. |
group | number | 32 | Group header height. |
filter | number | 36 | Filter bar height. |
pagination | number | undefined | Pagination bar height. |
striped | boolean | false | Enable alternating row colors. |
stripe | string | undefined | Custom class for striped rows. |
show | boolean | 'none' | 'partial' | 'full' | 'none' | Column border mode. 'partial' = 50% height, 'full' = 100%. true/false are backwards-compatible aliases. |
auto | number | undefined | Max width (px) that body-cell content may contribute to auto-size. Header width always wins. Per-column override via meta.. |
disable | boolean | undefined | Disable row hover background. |
no | string | 'No results found.' | Message when grid has no data. |
no | React | undefined | Custom empty-state component. |
row | Record<string, (data: TData) => boolean> | undefined | Conditional CSS classes on rows. |
show | boolean | undefined | Filter row visibility: true = always show, false = always hide, undefined = auto (show when ≥1 column has meta.). |
footer | Grid | undefined | Footer styling. |
header | Grid | undefined | Header styling and behavior. |
pagination | Grid | undefined | Pagination styling (container). |
GridHeaderConfig#
| Property | Type | Default | Description |
|---|---|---|---|
filter | string | undefined | CSS class for the filter row. |
menu | 'left' | 'right' | 'right' | Position of the menu button within header cells. |
show | boolean | true | Show menu trigger only on hover (true) or always visible (false). |
show | boolean | true | Show filter-active icon in header cells when column has an active filter. |
show | boolean | true | Show column separator lines between header cells (controlled by CSS vars). |
GridFooterConfig#
| Property | Type | Default | Description |
|---|---|---|---|
class | string | undefined | CSS class for the footer container row (background, borders, etc). |
cell | string | undefined | CSS class for individual footer cells. |
GridPaginationOptions#
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable pagination. With false, a client grid presents every filtered and sorted row as one list: no page is cut and a page size is ignored; row virtualization still limits what is mounted. A server grid keeps its server paging. |
mode | 'paginated' | 'infinite' | 'paginated' | Pagination mode. 'infinite' enables scroll-triggered batch-loading. Requires mode="server" — on a client-mode grid the setting is ignored (the grid falls back to standard pagination) and a dev-warn is emitted once per grid. |
position | 'top' | 'bottom' | 'both' | 'bottom' | Where to render pagination. |
page | number[] | [10, 20, 50, 100] | Available page sizes. |
show | boolean | true | Show "1 – 50 of 5,000" row-count summary. |
show | boolean | true | Show page-size selector dropdown. |
show | boolean | true | Show "Page 1 of 100" page indicator. |
show | boolean | true | Show first/last page buttons (« »). |
show | boolean | true | Show prev/next page buttons (‹ ›). |
row | 'left' | 'right' | 'left' | Position of row-count summary. |
class | string | undefined | Additional CSS class on the pagination container. |
GridToolbarConfig#
import {type GridToolbarConfig} from '@workspace/active-grid';Configuration for the toolbar above the grid. Opt-in: only renders when
at least one feature is enabled (search., show, or
actions).
| Property | Type | Default | Description |
|---|---|---|---|
hidden | boolean | undefined | Force-hide the toolbar. |
class | string | undefined | CSS class on the toolbar container. |
search | { enabled, placeholder, class | undefined | Search input configuration (inline shape). See below. |
show | boolean | false | Show the Column Chooser button. Opens a dialog for toggling column visibility. Appears only when at least one column has enable. |
actions | React | undefined | Custom toolbar actions (buttons, dropdowns). |
Search input (inline shape)#
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable the search bar in the toolbar. |
placeholder | string | 'Search...' | Search input placeholder text. |
class | string | undefined | CSS class on the search input. |
debounce | number | 300 | Debounce delay for the global filter in ms. |
GridHeaderMenuConfig#
import {type GridHeaderMenuConfig} from '@workspace/active-grid';Per-column header-menu (3-dot button) configuration. Set globally via the
header prop or per-column via meta..
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether the menu is enabled. |
show | boolean | false | Show text labels above sections (e.g. "SORT"). |
class | string | undefined | CSS class for the dropdown container. |
item | string | undefined | CSS class for individual menu items. |
separator | string | undefined | CSS class for separators. |
features | { sort, pin, resize, visibility, grouping, reset } | All enabled | Which features to show. See below. |
Header-menu features (inline shape)#
| Property | Type | Default | Description |
|---|---|---|---|
sort | boolean | true | Ascending/Descending options. |
pin | boolean | true | Pin left/right options. |
resize | boolean | true | Auto-size options. |
visibility | boolean | true | Hide column option. |
grouping | boolean | true | Group-by column (only when enable is on). |
reset | boolean | true | Reset table layout. |
GridRowMenuConfig#
import {type GridRowMenuConfig} from '@workspace/active-grid';Row context-menu (right-click) configuration via the row prop.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Whether the context menu is enabled. false offers no grid menu on any row; the browser keeps its own. |
class | string | undefined | CSS class for the menu container. |
item | string | undefined | CSS class for individual items. |
separator | string | undefined | CSS class for separators. |
sub | string | undefined | CSS class for sub-menu containers. |
features | { copy, export, pin, reset } | All enabled | Which default features to show. |
Row-menu features (inline shape)#
| Property | Type | Default | Description |
|---|---|---|---|
copy | boolean | true | The copy actions, named by scope (selection, cell, row, selected rows). Also the copy-only menu of a generated row. |
export | boolean | true | Export section (CSV, Excel, JSON). |
pin | boolean | true | Row pinning options. |
reset | boolean | true | Reset table layout. |