apsw-gridwright
Documentation

API reference

Every prop, option and column field, with its type, its default and what it does. The feature pages explain why; this page is for looking something up.

It is kept in step with the types: a change to a prop, an option or a column field updates this page in the same change. The types themselves are the final word: GridwrightProps and UseGridwrightOptions in src/react/types.ts, ColumnDef in src/core/types.ts, and each add-on's options type beside its factory.

TRow is your row type throughout.

<Gridwright /> and useGridwright#

<Gridwright /> takes everything in the three tables below. useGridwright(options) takes the Data and engine and Features tables and returns the instance you pass to <Gridwright instance> or <GridwrightProvider instance>.

Data and engine#

PropTypeDefaultWhat it does
columnsGridwrightColumn<TRow>[]requiredThe columns. See Columns. May be written inline: the engine only re-resolves them when something it reads changes.
datareadonly TRow[]—An in-memory array. Replacing the array updates the grid. Use this or dataSource, not both.
dataSourceDataSource<TRow>—Anything else: createRemoteDataSource, createRestDataSource, createWindowedDataSource, or your own. A new source object refetches.
getRowId(row, index) => RowIdthe row's id property, else its indexA row's stable identity. Selection, row menus and edits are keyed on it.
initialQueryPartial<GridQuery>no sort, no filters, empty search, page 0The query the grid starts with: sort, filters, search, pagination.
pageSizenumber25Rows per page. Under virtualRows(), the size of each fetched window. Changing it on a live grid applies it. A value that is not a finite number (Infinity, NaN) is not a page size and falls back to 25; for every row without pages, list virtualRows().
selectionMode'none' | 'single' | 'multiple''none'Whether rows can be selected, and how many. multiple also draws the checkbox column, which coreAddons({ selection: { checkboxes: false } }) removes without turning selection off.
keepPreviousDatabooleantrueKeep the current rows on screen while the next ones load, instead of an empty table.
queryDebounceMsnumber0Waits this long after the last query change before fetching. Useful for a remote search box.
pluginsGridPlugin<TRow>[][]Engine plugins added to the core set. One named like a core plugin (gridwright:sorting) replaces it. Reconciled by name on a live grid. See plugins.
corePluginsbooleantruefalse installs none of the core pipeline stages: filtering, search, sorting and pagination are then yours.
onQueryChange(query: GridQuery) => void—After the sort, filters, search or page change.
onSelectionChange(ids: RowId[], rows: TRow[]) => void—After the selection changes. Under treeData(), rows are your rows, not tree nodes.
onError(error: GridError) => void—After a fetch fails. The grid shows the failure either way.

Features#

PropTypeDefaultWhat it does
addonsGridAddon<TRow>[][]Features, after the core add-ons: [search(), columnFilters(), exportMenu()]. A changed list of names remounts the grid. See add-ons.
coreAddonsGridAddon<TRow>[] | falsecoreAddons()The add-ons every grid starts with: sorting, selection, pagination, the stale-rows notice. coreAddons(options) configures one ({ selection: { checkboxes: false } }); any list replaces them; false renders none.

Presentation and translation (<Gridwright /> only)#

PropTypeDefaultWhat it does
instanceGridwrightInstance<TRow>—Render an instance from useGridwright instead of building one. Its own add-ons apply; addons and coreAddons here are ignored.
aria-labelstring—The table's accessible name. Give every grid one, or a caption.
captionReactNode—A visible <caption> for the table.
classNamestring—Added to the root element.
classNamesPartial<GridwrightClassNames>—A class for each part. See Class names.
toolbarReactNode—Your own toolbar content, after every add-on's. Its presence makes the toolbar render.
footerReactNode—Rendered after the table and after every add-on's below-table content.
onRowClick(row: GridRow<TRow>) => void—A click on a row. Clicks on a checkbox or on an editable cell do not reach it.
localestring | LocaleCatalog'en'A pack from apsw-gridwright/locales (pl, de, es, fr) translates everything. A bare tag ('en-GB') changes number formatting, plural rules and direction only.
messagesMessageOverrides—Override single strings: the shell's ('status.empty') or an add-on's under its name ('gridwright:filters.apply').
translate(key, values) => string—Hand every string to an i18n library (translate={t}). Returning the key itself falls back to the catalog. See translation.
labelsPartial<GridwrightLabels>—The shell's own strings, applied last: loading, empty, errorTitle, retry, rowsShown(from, to, total, exact), rowsTotal(count).

What useGridwright returns#

FieldTypeWhat it is
apiGridApi<TRow>The engine: setSort, setFilter, setSearch, setPage, toggleRowSelection, refresh, use, removePlugin and the rest (README, "Engine").
stateGridState<TRow>status, query, rows, totalRows, isTotalExact, selectedIds, error, version, meta.
columnsResolvedColumn<TRow>[]The columns with their defaults applied.
definitionsMap<string, GridwrightColumn<TRow>>Your column objects by id, as configured by the add-ons.
contributionsResolvedContributions<TRow>Every add-on's contribution, resolved.
announce(sentence: string) => voidSay a sentence through the grid's live region.

Columns#

A column is a plain object. Only id is required.

What the engine reads#

FieldTypeDefaultWhat it does
idstringrequiredUnique. Also the property read when there is no accessor, and the {placeholder} name in report templates.
headerstringthe idThe header text, and the column's name in announcements and filter labels.
accessorkeyof TRow | (row) => valuethe property named by idHow the value is read from a row.
sortablebooleantruefalse: no sort button, and toggleSort and the in-memory sort ignore the column.
filterablebooleantruefalse: no filter button under columnFilters(), and the in-memory filter ignores a filter on it. A source that filters for itself still receives whatever was set through api.setFilter.
searchablebooleantruefalse: global search skips this column.
hiddenbooleanfalseNot rendered. It still sorts, filters and searches.
widthnumber | stringautomaticThe column's width: pixels, or any CSS length.
minWidthnumber—The narrowest it may get, in pixels.
align'start' | 'center' | 'end''start'Text alignment of the header and the cells. end for numbers.
comparator(a, b, rowA, rowB) => numbertype-aware: numbers numerically, text by locale, empty values together at one endHow two values sort.
filterFn(value, filter, row) => booleanthe operator's standard testHow a filter matches a value, when the pipeline filters in memory.
formatValue(value, row) => stringthe value as text; empty for null, ISO for a date, joined for an arrayThe text a cell shows by default, that search matches, and that exports write.
exportValue(value, row) => stringformatValueThe text exports write, when it should differ from the screen: 120000 rather than $120,000.
exportablebooleantruefalse leaves the column out of every export. For a column of buttons.
metaRecord<string, unknown>—Anything of yours. The engine never reads it; a plugin or renderer can.

What React renders#

FieldTypeDefaultWhat it does
cell(context) => ReactNodethe column's textThe cell's content. context: value, row, rowId, rowIndex, column, api.
headerCell(context) => ReactNodeheaderThe header's content, inside the sort button. context: column, api, sortDirection.
icon(context) => ReactNode—A glyph before the cell's content, per row, hidden from screen readers.

An actions column is an ordinary column whose cell renders buttons. It holds no value, so it opts out of everything that reads one. Buttons in a cell keep their own clicks: pressing one neither selects the row under selectOnRowClick nor opens a rowActions() menu. Name the row in each button's accessible name, because "Delete" read in a list of forty says nothing about which row.

tsx
const actions: GridwrightColumn<Person> = {
    id: 'actions',
    header: 'Actions',
    accessor: () => null,
    sortable: false,
    filterable: false,
    searchable: false,
    exportable: false,
    layout: { pinned: 'right', resizable: false }, // read by columnLayout(), if listed
    cell: ({ row }) => (
        <button type="button" aria-label={`Delete ${row.name}`} onClick={() => remove(row.id)}>
            <TrashIcon aria-hidden="true" />
        </button>
    ),
};

<Gridwright columns={[...columns, actions]} data={people} />

Beside a column like this, list rowActions({ items, trigger: 'contextmenu' }) if you want the menu as well. A menu previewed on hover opens over the row, between the pointer and the buttons.

Read by add-ons#

FieldRead byTypeWhat it does
filtercolumnFilters()ColumnFilterOptionsWhat the column holds, and so which conditions and input its filter offers.
editinlineEditing()ColumnEditOptions<TRow>Makes the column editable in place.
layoutcolumnLayout()ColumnLayoutColumnOptionsWhether the column resizes, which edge it pins to, and whether it may be hidden.
aggregategrouping()'sum' | 'avg' | 'min' | 'max' | 'count' | (values, rows) => unknownAggregates this column within each group and across the grand total.

filter

FieldTypeDefaultWhat it does
type'text' | 'number' | 'date' | 'select''text'Decides the conditions: text contains, starts with…; number greater than, between…; date on, after, before…; select is any of.
choices{ value, label }[]—For select: the values offered. value is compared and sent; label is shown, already translated.
operatorsFilterOperator[]every operator of the typeNarrows or reorders the conditions. The first is where a new filter starts.

edit

FieldTypeDefaultWhat it does
editableboolean | (row) => booleantrueWhether this row's cell can be edited.
inputType'text' | 'number' | 'date' | 'checkbox' | 'select''text'The built-in editor's input.
choices{ value: string, label: string }[]—For select: the options.
parse(input: string) => valuenumbers for number, booleans for checkbox, else the textTurns what was typed into the value committed.
editor(context) => ReactNode—Replaces the built-in editor. context: value, row, rowId, column, commit(next), cancel().

layout

FieldTypeDefaultWhat it does
resizablebooleantruefalse removes the resize handle from this header.
pinned'left' | 'right'unpinnedWhich edge the column starts frozen against. Applied as a logical offset, so left is the start of the row.
hideablebooleantruefalse shows the column in the picker checked and refusing to be unchecked.
movablebooleantruefalse keeps the column where it is, and stops anything being moved across it.
maxWidthnumbernoneThe widest the column may be dragged.

The column's own width and minWidth are the starting width and the floor; see column layout.

An add-on of yours adds its own column field by module augmentation; see column options of your own.

Core add-ons#

On by default through coreAddons(). Configure one without rebuilding the list by passing coreAddons(options) back to the prop of the same name:

tsx
// Multi-row selection, no checkbox column.
<Gridwright selectionMode="multiple" coreAddons={coreAddons({ selection: { checkboxes: false } })} ... />
OptionTypePassed to
sortingSortingOptionssorting()
selectionSelectionOptionsselection()
paginationPaginationOptionspagination()

Dropping one entirely is still a list operation: coreAddons().filter((a) => a.name !== 'gridwright:pagination'). coreAddons={false} drops all four.

sorting(options)#

OptionTypeDefaultWhat it does
multiSortbooleantrueShift-activating a header adds its column to the sort instead of replacing it. With it on, a Shift-activated sorted column reverses in place and then leaves the sort, and the button's title says what Shift does.

While more than one column is sorted, each sorted header's button holds <span class="gw-sort-priority" aria-hidden="true">{n}</span>, its 1-based place in query.sort.

Message (gridwright:sorting.*)English
ascending, descending, clear"Sort ascending", "Sort descending", "Clear sort": the button's title, naming the next action
actionWithShift"{action} (Shift: keep other columns sorted)": the title while multiSort is on
sortedAscending, sortedDescending, sortCleared"{column}, sorted ascending", "{column}, sorted descending", "{column}, not sorted"
sortedAscendingPriority, sortedDescendingPriority"{column}, sort priority {priority}, sorted ascending" (or descending), while more than one column is sorted

selection(options)#

OptionTypeDefaultWhat it does
checkboxesbooleanon when selectionMode is multipleThe checkbox column, with a select-page checkbox in its header.
countbooleantrueThe "3 selected" count in the toolbar, shown only while a toolbar is shown for something else.
selectAllbooleantrueThe select-page checkbox in the checkbox column's header. Off, the header holds the column's name (selectColumn, "Selection") for screen readers only, and the row checkboxes stay.
selectOnRowClickbooleanfalseClicking a row toggles its selection. A click on a button, link, input, label or menu item inside the row goes to that control instead, and a click that ends a text selection selects nothing. With cellNavigation(), Space on a focused cell that holds no control toggles its row. Rows get .gw-row--selectable (cursor: pointer).

The selection itself is the engine's: selectionMode, onSelectionChange, api.toggleRowSelection.

tsx
// A list you select from by clicking rows, operable by keyboard through cellNavigation().
<Gridwright
    selectionMode="multiple"
    coreAddons={coreAddons({ selection: { checkboxes: false, selectOnRowClick: true } })}
    addons={[cellNavigation()]}
    ...
/>

checkboxes: false needs a replacement control. The selection state, aria-selected on rows and aria-multiselectable on the table keep working, but without checkboxes the only built-in way to toggle a row is selectOnRowClick, and its keyboard route is Space on a focused cell, which exists only with cellNavigation(). checkboxes: false with neither is a grid nobody can select from by keyboard; drive it yourself through api.toggleRowSelection.

A row click also runs the grid's own onRowClick, first, so it sees the selection as it was before the click. With rowActions() at its default trigger, or 'click', the same click also pins the row menu. Pass rowActions({ items, trigger: 'hover-contextmenu' }) to leave the click to selection: the menu still previews on hover and focus, and pins on right-click and the context-menu key.

pagination(options)#

OptionTypeDefaultWhat it does
pageSizeOptionsnumber[][10, 25, 50, 100]The choices in the rows-per-page menu.

staleNotice()#

No options. The banner above the table when a refresh failed and the previous rows are still shown.

What the core add-ons decide#

Exported from apsw-gridwright/react as plain functions, for another view of sorting, selection or pagination: an add-on of your own with the same name, or the MUI package, which is built on these and nothing private. A view that calls them says and does what the native one does.

FunctionReturns
ariaSortOf(direction)'ascending' | 'descending' | 'none', for the header cell
nextSortAction(direction)the message key for the next action: 'ascending' | 'descending' | 'clear'
sortTitleOf(direction, multiSort, t)the sort control's title, with the Shift hint while multiSort is on
sortPriorityOf(sort, columnId)the column's 1-based place in the sort, or 0 when it is unsorted or the only sorted column
sortAnnouncement()the sorting add-on's announce contributor (priority 20)
pageSelectionOf(state){ all, some }: the select-page checkbox's checked and indeterminate state
selectionTableAttributes(grid)aria-multiselectable for the table
selectionRowAttributes(row, grid, { selectOnRowClick })aria-selected, the selected and selectable classes, and the guarded row click
selectionKeyDown(event, grid)Space on a focused cell with no control; true when handled
pageRangeOf(state){ from, to, total }, with total null when the source sent no count. Under grouping() it counts records, not the group headers among the rows: total is every matching record whatever is collapsed, and from/to are the records on the page (0 and 0 when it holds only headers)
pageSizeChoices(options, pageSize)the page sizes to offer, including the grid's own
DEFAULT_PAGE_SIZE_OPTIONS[10, 25, 50, 100]
pageFocusAfterChange(pressed, disabled)which page button takes focus after a page change, or null

MUI: apsw-gridwright-mui#

A second package, for applications built on MUI (@mui/material 7 or 9). Pass muiAddons() where coreAddons() would go. Its README covers installation and what does and does not follow the theme.

ExportSignatureWhat it does
muiAddons(options?: CoreAddonOptions) => GridAddon[][muiTheme(), muiSorting(options.sorting), muiSelection(options.selection), muiPagination(options.pagination), staleNotice()]
muiTheme() => GridAddonThe grid's --gw-* tokens, font and data-gw-theme from the MUI theme in context, on the root. var(--mui-…) references for a cssVariables theme. Name gridwright:mui-theme.
muiSorting(options?: SortingOptions) => GridAddonTableSortLabel as a real button. Name gridwright:sorting.
muiSelection(options?: SelectionOptions) => GridAddonMUI Checkboxes, every selection() option. Name gridwright:selection.
muiPagination(options?: PaginationOptions) => GridAddonTablePagination with a native select, the grid's range text, and Previous and Next from hasPreviousPage and hasNextPage. Name gridwright:pagination.
muiTokens(theme: Theme) => GridTokensThe values muiTheme() applies.
MUI_THEME_ADDON'gridwright:mui-theme'

The views take the options, defaults and names of the add-ons they replace, so a grid cannot list both views of one feature, and locale packs and message overrides apply to both.

Add-ons#

No options. The search box, first in the toolbar. What it matches is set per column with searchable and formatValue; how fast it asks a server, with queryDebounceMs.

columnFilters()#

No options. Per column: filterable and filter (see Columns). See filtering.

exportMenu(options)#

OptionTypeDefaultWhat it does
formats('csv' | 'excel' | 'markdown' | 'print' | CustomExportFormat)[]['csv', 'markdown', 'print']The menu's entries, in order. A custom format is { id, label, serialize, name? }; markdownReportFormats(...) makes two.
filenamestring | () => stringexport-YYYY-MM-DDThe saved file's name, without the extension.
scope'all' | 'page' | 'selected'chosen by the reader, starting from allFixes which rows every export covers and hides the choice.
csvCsvOptions—delimiter (,), newline (\r\n), bom (true), header (true), escapeFormulas (true).
excelExcelOptions—sheetName (Sheet1).
printPrintOptions—title, styles (replaces the print stylesheet), lang, direction.
serializersRecord<string, ExportSerializer>—Replaces a format's writer, by id.
onError(error: unknown) => voidlogs to the consoleA failed export. The reader sees a translated sentence either way.

markdownReportFormats(options) returns two formats for formats: a .md file and a PDF through the print dialog.

OptionTypeDefaultWhat it does
idstringrequiredNamespaced, acme:roster. The entries are <id>:markdown and <id>:pdf.
labelstringrequiredThe report's name, translated. The menu shows "Roster (Markdown)" and "Roster (PDF)".
templatestring | (row, index) => stringrequiredOne block per row. {columnId} writes that column's export text.
header, footerstring | (rows) => string—Above and below the rows.
separatorstringa blank lineBetween row blocks.
titlestring | (rows) => stringthe labelThe printed document's title, which browsers offer as the PDF's name.
printPrintOptions without title—styles, lang, direction.
outputs('markdown' | 'pdf')[]bothWhich entries to offer.
labels{ markdown?, pdf? }—Replaces an entry's whole label.

See exporting.

rowActions(options)#

OptionTypeDefaultWhat it does
itemsBubbleMenuItem<TRow>[]requiredThe actions.
trigger'hover' | 'click' | 'contextmenu' | 'hover-contextmenu' | 'both''both'What opens the menu. both: hover and focus preview it, a click or the context-menu key pins it. hover-contextmenu: the same without the left click, for a grid with selectOnRowClick.
placement'top' | 'bottom''top'Over the row, or hanging under it.
classNamestring—Added to the menu.

An item

FieldTypeDefaultWhat it does
idstringrequiredUnique within the menu.
labelReactNoderequiredWhat the item shows.
onSelect(row: GridRow<TRow>) => voidrequiredRuns the action. Under treeData() the row holds a node and under grouping() a member wrapper; rowDataOf(row) gives your row either way.
disabledboolean | (row) => booleanfalseShown, but cannot be chosen.
hidden(row) => boolean—Not shown for this row.
separatorBeforebooleanfalseA separator above the item.
destructivebooleanfalseStyled as destructive.

inlineEditing(options)#

OptionTypeDefaultWhat it does
commit(rowId, columnId, value) => void | Promise<void>requiredStores an edit. A rejected promise shows the error on the cell. Under treeData(), controller.updateRow applies it optimistically and reverts on failure.

Which columns are editable is the column's edit (see Columns).

Starting and finishing an edit. An editable cell renders its value inside a button.

ToDo
start editingclick the cell, or Tab to it and press Enter or Space; under cellNavigation(), arrow to it and press Enter or F2
save a text, number or date editEnter, or click or Tab away
discard itEscape
save a selectchoose an option; Escape or leaving it discards
save a checkboxtoggle it

A click on an editable cell opens the editor and does not reach onRowClick. When commit rejects, the cell shows the error and a screen reader hears it.

When the editor closes from the keyboard (Enter, Escape, choosing an option, toggling a checkbox), focus returns to the cell's button, so a keyboard reader stays in the grid. Clicking away leaves focus where the click put it.

treeData(options)#

OptionTypeDefaultWhat it does
getRowId(row) => RowIdrequiredThe row's identity. Every placement of one row shares it.
getChildren(row) => TRow[] | undefined—Children carried on the row. Use this or getParentIds.
getParentIds(row) => RowId | RowId[] | null—Parents named by the row. The only shape where a row sits under several parents.
hasChildren(row) => booleanfalseA row has children not loaded yet, so it gets a toggle before they arrive.
loadChildren({ row, rowId, node, signal }) => Promise<TRow[]>—Fetches children on first expand, once per row wherever it appears.
defaultExpandedDepthnumber0Expand down to this depth on first render. 0 shows the roots only.
maxDepthnumber64Rows deeper than this are not placed. A cycle is detected and marked separately.
treeColumnIdstringthe first visible columnThe column that carries the indentation and the toggle.
keepAncestorsOfMatchesbooleantrueWhile filtering or searching, keep the rows above a match.
onCommit(change) => void | Promise<void>—Stores an insert, move, edit or removal after the tree applied it. Throwing reverts it.
onExpandedChange(nodeIds: string[]) => void—After a node opens or closes.
controllerRef(controller | null) => void—Receives the controller (insertRow, moveNode, removeNode, updateRow, toggle…) and null on unmount.

See tree data.

grouping(options)#

Collapsible group headers, one per distinct value of a groupBy column, and a column's aggregate computed for each group and, with summaryRow, for the whole result. Runs after filtering, search and sorting, so a group sees the rows they left. Cannot be listed with treeData().

OptionTypeDefaultWhat it does
groupBystring[]requiredColumn ids, in nesting order. The first groups the whole set; the next groups within it.
summaryRowbooleanfalseA grand-total row in the table footer, aggregating across every matching row.
defaultExpandedbooleantrueWhether a group starts expanded.
serverGroupedbooleanfalseThe data source already returns group headers and member rows itself; the local stage is skipped.

A column declares what it aggregates with aggregate (above): sum, avg, min, max, count, or (values, rows) => unknown for anything else. Grouping needs every matching row in memory, the same as a plain array: it refuses a source that paginates for itself unless serverGrouped says the source grouped already. ungroupedRows(rows) (from apsw-gridwright) turns the rows api.getMatchingRows() answers on a grouped grid — group headers and member rows both — into your rows alone, for an export or anything else that wants a flat list.

columnLayout(options)#

OptionTypeDefaultWhat it does
initialPartial<ColumnLayoutState>—A layout to start from: widths, pinning and visibility, as onChange last reported them.
onChange(layout: ColumnLayoutState) => void—Called after a change is committed and rendered. Never during a drag, and never on mount.
pickerbooleantruefalse leaves the toolbar alone, for a <GridColumnPicker /> you place yourself.
resizablebooleantruefalse removes every resize handle.
reorderablebooleantruefalse makes no header a drag source and removes the Ctrl+arrow shortcut.
canChange(change: ColumnLayoutChange, layout: ColumnLayoutState, resolved: ColumnLayoutResolved) => boolean—Called before every committed change. false refuses it. Narrows the add-on's own rules and never widens them. layout is the reader's overrides only; resolved answers what is actually pinned, hidden, ordered and how wide.
defaultWidthnumber150The width of a column that declares no pixel width of its own.
minWidthnumber50The floor under every column, beneath the column's own minWidth.
extraColumnWidthnumber48The width used for another add-on's column, such as the selection checkbox.

Resize handles in every header, draggable headers, a column picker in the toolbar, and sticky pinned columns. The table gets table-layout: fixed while this add-on is listed, and its cells truncate instead of wrapping. Reordering is by drag or by Ctrl/Cmd + arrow on a focused header, and the order reaches the engine, so an export follows it. Inside the grid, useColumnLayout() gives the controller the picker, the handles and the drag all use; see column layout.

density(options)#

density({ initial?, levels?, rowHeights?, control?, onChange? }) lets a person choose compact, comfortable or spacious rows, and puts data-gw-density on the root. Defaults: comfortable (the grid as it is), all three levels, rowHeights { compact: 32, spacious: 52 }, a toolbar select (control: false for none). onChange(level) is how you store the choice; initial is how you restore it; the package keeps no storage. useDensity() returns { level, levels, setLevel } (it throws where the add-on is not listed) and useOptionalDensity() returns null there. The active height is published to virtualRows(). See density.md.

wcag(options)#

wcag() sets the grid to WCAG 2.2 AA where the default is not, and puts data-gw-wcag="aa" on the root. options is an empty object, reserved so a level can be added later. It adds no control and no message. The stylesheet reads the attribute: the row checkboxes and the tree, group and row-detail toggles are 24 px (--gw-target-min), --gw-text-muted, --gw-border and --gw-border-strong take values that pass 1.4.3 and 1.4.11, and under forced-colors: active the focus ring, cursor cell, selected row, sort state, resize line and disabled controls use system colours. A grid that does not list it is unchanged. With muiTheme() the theme's colours win. With columnLayout() it also adds move and width buttons to each row of the column picker. It also scrolls the table's wrapper back when focus lands under the sticky header or under a column pinned to either edge, so a focused element is never left covered (a handler on the root's onFocus). useWcagEnabled() returns whether the add-on is listed (false elsewhere, never throws). See accessibility.md.

responsive(options)#

responsive({ initialWidth? }) follows the width of the grid's container. A column's responsive: { hideBelow } hides it below that many pixels (view only: still sorted, filtered, searched, exported). useContainerWidth() returns the width in pixels, or null. See responsive.md. stackBelow draws the rows as cards below that many pixels (ignored beside virtualRows()); initialWidth is the width assumed until one is measured. An add-on's contribution may carry whenNarrow: { below, contribution }, viewHiddenColumns and cardLayout; useMediaQuery(query) is the hook rowActions() uses for (hover: none).

rowDetail(options)#

An expandable panel under a row. See expandable rows.

OptionTypeDefaultWhat it does
render({ row, data, grid, close }) => ReactNoderequiredThe panel's content. Called only while it is open; null renders no panel row. data is your row, unwrapped under treeData().
hasDetail(row, grid) => booleanevery rowfalse draws no toggle for that row.
rowLabel(data, grid) => stringthe first visible column's textNames the row in the toggle and the panel.
initialExpandedreadonly RowId[][]Open on the first render, and never reported to onExpandedChange.
onExpandedChange(expanded: RowId[]) => void—After a change is committed and rendered.
singlebooleanfalseOpening one panel closes the other.
toggle'start' | 'end' | 'none''start'Which side the toggle column goes, or none at all.
persistAcrossPagesbooleantrueKeep an expansion when the query moves the row away.
canToggle(rowId, expanded) => boolean—Called before every committed change. false refuses it. Narrows and never widens.
classNamestring—Added to the panel's region.
controllerRef(controller | null) => void—Receives RowDetailController once, and null on unmount.

useRowDetail() returns the controller from anywhere inside the grid: expanded, isExpanded, allows, expand, collapse, toggle, expandAll, collapseAll. expandAll() covers the rows the grid is currently holding and makes no claim about pages it has not fetched.

Throws when listed with virtualRows(): windowing places rows by a fixed height and a panel is as tall as its content.

cellNavigation(options)#

Spreadsheet-style cursor movement: one Tab stop into the grid, then the arrow keys. See accessibility.

OptionTypeDefaultWhat it does
initialCellActiveCellthe first cell of the first rowWhere the cursor starts.
onActiveCellChange(cell: ActiveCell | null) => void—Called after the cursor moves and the cell has rendered. Never on mount.
includeExtraColumnsbooleantrueWhether another add-on's columns — the selection() checkbox, the rowDetail() toggle — are reachable with the arrows.
copybooleantrueWhether the platform's copy shortcut copies from the grid. false leaves copying to the browser.
headerRowbooleanfalseWhether the header row is part of the cursor's grid. On, ArrowUp from the first row reaches the headers, Enter/Space sorts, Shift+Enter adds the column to the sort, and the sort buttons leave the Tab order.
KeyMoves to
ArrowUp / ArrowDownthe same column in the previous / next loaded row
ArrowLeft / ArrowRightthe previous / next column, in reading direction — reversed under dir="rtl"
Home / Endthe first / last column of the row
PageUp / PageDownone page of rows, clamped to what is loaded
Ctrl/Cmd + Homethe first cell; also returns to page one when the total is exact
Ctrl/Cmd + Endthe last cell of the last loaded row
ArrowRight / ArrowLeft on a tree nodeexpands / collapses it before moving

No key fetches a page. A paginating source that sends no total leaves isTotalExact false, and the grid then knows only that another page exists — so there is no last row to jump to. Ctrl+End stops at the last row held rather than inventing one or paging until the source runs out.

Arrow keys inside a form control are left alone, so an inline editor, a <select> in a cell renderer or anything contenteditable keeps them.

Controls inside cells are operated from the cell. A button, link or checkbox inside a body cell is taken out of the Tab order, so Tab leaves the grid in one press rather than visiting every row's checkbox. Enter, Space or F2 on the focused cell operates it instead: a button, link or checkbox is clicked, and a text field or select is focused. When a cell holds several controls, the first that is not a disclosure toggle wins, so Enter on a tree cell with an editable value edits it while the arrows expand and collapse. A key pressed on the control itself stays the control's.

The built-in controls do this themselves. A control in a cell renderer of your own does it with useCellTabIndex(), which returns -1 while the cursor visits that cell and undefined otherwise:

tsx
function ProfileLink({ person }: { person: Person }) {
    return <a href={person.url} tabIndex={useCellTabIndex()}>{person.name}</a>;
}

// A component, so the hook runs inside it rather than inside the grid's render.
const columns = [{ id: 'name', header: 'Name', cell: ({ row }) => <ProfileLink person={row} /> }];

Pass the column id from an extra column of your own. With includeExtraColumns: false the cursor does not visit extra columns, so their controls, the selection checkbox among them, keep their Tab stops. The header's sort buttons keep theirs too, unless headerRow: true brings the header into the cursor.

The header row is opt-in. With headerRow: true the grid is one Tab stop including the headers: the cursor opens on the first row of data, ArrowUp reaches the header of the same column, Ctrl/Cmd + Home goes to the first header, and Enter, Space or F2 on a header sorts by it (Shift+Enter adds the column to the sort, as Shift-click does). The built-in sort button, and the MUI one, leave the Tab order in that mode. A sorting view of your own does the same with useHeaderCellTabIndex(), which returns -1 while the header is visited and undefined otherwise. Controls beside the label, a filter button or a resize handle, keep their Tab stops. A cursor on the header has rowId HEADER_ROW_ID, which onActiveCellChange and useCellNavigation().activeCell report. Under grouping() the cursor passes over a group header: it is one cell spanning the row, and its toggle is a Tab stop.

Copying. The platform's copy shortcut — Ctrl+C, Cmd+C or Ctrl+Insert, whichever the reader's system uses — copies the selected rows that are loaded, with a header row, or, with nothing selected, the cell under the cursor. The clipboard gets tab-separated text and an HTML table, so a spreadsheet pastes cells and a text editor pastes lines. Cell text is resolved exactly as an export resolves it (exportValue, then the column's text), with the same formula guard, and every cell is escaped in the HTML. Text the reader selected with the pointer is copied as that text; a copy inside a form control is the control's; the selection checkbox cell and a column with exportable: false copy nothing of the grid's. Announced through the live region, under gridwright:cell-navigation as copiedRows (plural) and copiedCell.

It runs in the browser's own copy event rather than through navigator.clipboard, so it needs no permission, works on a plain-HTTP page and inside an iframe with no allow="clipboard-write", and behaves the same on Windows, macOS, Linux and ChromeOS. What a copy guards against, and what it leaves to you, is under Security.

useCellNavigation() gives a control of your own the same cursor — activeCell, columnIds, isActive(rowId, columnId) and focusCell(cell) — and useOptionalCellNavigation() returns null where the add-on is genuinely optional.

virtualRows(options)#

OptionTypeDefaultWhat it does
rowHeightnumber40Every row's height in pixels. Must match --gw-row-height.
heightnumber | string420The scrolling area's height.
overscannumber6Extra rows rendered above and below the viewport.
renderSkeleton(absoluteIndex: number) => ReactNodea grey barShown for a row on screen whose data has not arrived.

Replaces the page controls with the scrollbar. Inside the grid, useVirtualScroll() gives scrollToIndex(index). See virtualization.

urlSync(options)#

Keeps search, sort, filters and page in the URL. See the view in the URL.

UrlSyncOptions#

OptionTypeDefaultWhat it does
adapterUrlSyncAdapterthe browser's location, history and popstateWhere the parameters are read and written. Pass one to go through a router.
prefixstring''Put before every parameter name, for several grids on one page.
facetsreadonly ('search' | 'sort' | 'filters' | 'page' | 'size')[]all fiveWhich parts of the query go in the URL.
pushthe same['page']Changes that add a history entry. Every other change replaces the current one.
debounceMsnumber300Quiet before a replace is written. A push is written at once.
maxPageSizenumberthe larger of 100 and the grid's own page sizeThe largest size a URL may ask for.

UrlSyncAdapter is { getParams(): URLSearchParams; setParams(params, mode: 'push' | 'replace'): void; subscribe?(onChange): Unsubscribe }. getParams is read on every render; setParams receives every parameter, the grid's own and the rest.

The parameters are q, sort, f, page (from 1) and size, each written only when it differs from the grid's starting query. The linked query is applied as initialQuery, and as pageSize when the URL names a size, so it wins over the pageSize prop at mount. Under virtualRows() the page is not synced.

The codec is exported on its own:

FunctionSignatureWhat it does
serializeGridQuery(query, { prefix?, facets?, baseline? }) => URLSearchParamsThe query as parameters, holding only what differs from baseline (default createQuery()).
parseGridQuery(params, columns, { prefix?, facets?, baseline?, maxPageSize? }) => Partial<GridQuery>The parts of the query the parameters name validly. Never throws.
formatSearchParams(params) => stringA query string without the ?, with : and , left readable.

Parts#

For a layout composed by hand under <GridwrightProvider instance={useGridwright(...)}>.

PartPropsDefaultWhat it renders
GridwrightProviderinstance (required), classNames, onRowClick, locale, messages, translate, labels, children—Publishes the grid to the parts below it. Same meanings as on <Gridwright />.
GridRootclassName, children—The root element, the live region, the add-ons' providers and overlays. Include it.
GridToolbarchildren—Every add-on's toolbar items, then yours. Renders nothing when empty.
GridSlotname: 'aboveTable' | 'belowTable' | 'overlay' | 'tableFooter'requiredOne slot's add-on content, wherever you place it.
GridTablecaption, aria-label, children—The table and its scrolling wrapper.
GridHeadernone—The header row.
GridBodynone—The rows, or the body an add-on owns.
GridPaginationpageSizeOptions[10, 25, 50, 100]Page controls, anywhere.
GridStaleNoticenone—The failed-refresh banner.
GridSearchclassName—The search box.
GridExportMenuevery exportMenu option, className—The export menu. useGridExport(options) is the same without the menu.
ColumnFilterTriggercolumnId (required), className—One column's filter button. Needs columnFilters() listed.
GridFilterClearclassName—"Clear filters", while any filter is on.
BubbleMenuitems (required), trigger, placement, className, aria-labelboth, topA row menu over rows it did not render.
GridColumnPickerclassName—The column picker menu: visibility and pinning per column. Needs columnLayout() listed.
GridResizeHandlecolumnId (required), className—One column's resize handle. Needs columnLayout() listed.
GridVirtualBodycontainerRef (required), rowHeight, overscan, renderSkeleton40, 6A windowed body. virtualRows() renders it for you.
GridRowViewrow, position (required), style—One row with every add-on's attributes and extra columns, for a body of your own.
GridDetailTogglerowId (required), className—One row's detail toggle. Needs rowDetail() listed.
GridRowDetailrowId (required), children, className—The presentational panel row, for a body composed by hand.
GridRowOrCustomrow, position (required), style—The same, plus any row an add-on renders instead of it and any rows contributed after it. What a body of your own should render per row.
columnCountOfgrid—Not a part but a function: how many cells a row spans, extra columns included. The colSpan for a row that covers the table.

Class names#

classNames on <Gridwright /> or <GridwrightProvider> adds your class beside the built-in gw- class of each part. The stylesheet's CSS custom properties are the other way to theme; see theming.

KeyElementBuilt-in class
rootthe grid's outer elementgw-root
toolbarthe toolbargw-toolbar
searchthe search boxgw-search
tableWrapperthe table's scrolling wrappergw-table-wrapper
table<table>gw-table
thead<thead>gw-thead
headerRowthe header <tr>gw-header-row
headerCelleach <th>gw-header-cell
tbody<tbody>gw-tbody
roweach body <tr>gw-row
rowSelecteda selected row, as well as rowgw-row--selected
celleach body <td>gw-cell
statusthe loading, empty or error cellgw-status
paginationthe page controlsgw-pagination
stalethe failed-refresh bannergw-stale
filterTriggera header's filter buttongw-filter-trigger
filterDialogthe filter dialoggw-filter-dialog

Edit this page on GitHub