Skip to main content

Sheets

Sheets are spreadsheet-style tabs over one grid instance and one row model: a Data sheet and any number of pivot sheets share the same rows, edit history, and transactions. Each sheet is a live view state — columns, sort, filters, grouping, aggregates, pivot configuration, group expansion, and page. Switching tabs captures the sheet you leave and applies the one you enter.

Supplying the sheets option mounts the tab strip in the footer's left zone; the footer's three zones read tabs · aggregation · pagination, and each zone appears only when its feature is on.

Loading interactive example…
React
const [sheets, setSheets] = useState<GridSheet[]>([{ id: "data", name: "Data" }]);
const [activeSheetId, setActiveSheetId] = useState<string | null>("data");

<Grid
rowData={rows}
columnDefs={columns}
pagination
toolbar={{ pivot: true }}
sheets={{
sheets,
activeSheetId,
onChange: setSheets, // persist anywhere — the app owns the list
onActiveSheetChange: setActiveSheetId,
}}
/>

Working the tab strip

  • + appends a blank pivot sheet ("Pivot N"): pivot mode on, no roles assigned yet, with an inline header hint until an aggregate is chosen. Columns and filters carry over from the sheet you pressed + on, and so does its grouping — turning pivot mode off on the new sheet lands on that view.
  • Double-click a tab (or press F2 on it) to rename inline.
  • Right-click a tab for Rename / Change color / Duplicate / Delete. Duplicating the active sheet copies its live state (and its color); the last remaining sheet can't be deleted.
  • Change color picks from the palette, or clears the color with None. An inactive tab wears its color as a tint over the tab fill and the active one shows it solid in its underline, so the label keeps the theme's own contrast in light and dark whatever color is chosen. The color lands on GridSheet.color and is reported through onChange like any other change — it is sheet metadata, not view state, so it survives every switch untouched.
  • Ctrl+PageDown / Ctrl+PageUp switch to the neighboring sheet — the spreadsheet convention. The chords are claimed only while the tab strip is enabled.
  • The strip is an ARIA tablist with roving tabindex: arrow keys move between tabs, Enter/Space activates one, and the active tab names the grid for assistive technology (unless the app supplies its own ariaLabel).

Tab colors

colors replaces the built-in palette with the application's own (SheetTabColor[]{ color, name? } entries), so the menu offers a brand set rather than a superset:

sheets: {
sheets,
colors: [
{ name: "Revenue", color: "#0f766e" },
{ name: "Ops", color: "#b45309" },
{ color: "#6d28d9" }, // unnamed: labelled (and announced) as "#6d28d9"
],
}

Any CSS color works, and no light/dark variant is needed — the tint and the underline both derive from the one value. Because a color is only ever tinted under the label, an entry never has to clear a contrast bar.

A function makes the palette sheet-specific. It is asked each time a tab menu opens, with the sheet it was opened on, so it can also key off live application state:

sheets: {
sheets,
colors: (sheet) => (sheet.id === "budget" ? BUDGET_COLORS : BRAND_COLORS),
}

customColor adds a Custom… entry below the palette that opens the platform's own color picker — the browser's, so it is the picker the user already knows, keyboard-accessible and translated, with no in-grid picker UI to maintain. It commits when the picker confirms (dismissing it changes nothing), always as #rrggbb. It takes the same two forms as colors, and it is off by default: a palette is usually a palette on purpose.

sheets: {
sheets,
colors: BRAND_COLORS,
customColor: true, // or: (sheet) => sheet.id !== "budget"
}

When a sheet wears a color the palette does not offer — picked with Custom…, set programmatically, or left behind by a palette since changed — the check mark and the chip move to the Custom… entry, so the menu still shows what the tab is wearing.

An empty colors: [] — or an empty return from the function, which makes it per sheet — drops Change color from the menu entirely, which is how an application opts out of tab colors. With customColor on, the picker stands on its own: colors: [] then means "any color, no presets". Either way GridSheet.color is still honored: a color set programmatically, or persisted from a palette since changed, paints its tab as usual — the menu just shows no entry checked, and None still clears it.

The application owns the list

SheetsOptions mirrors savedViews: the grid renders and optimistically updates the supplied list, then reports every complete next list through onChange — persistence is the application's job. A sheet's state is a plain GridViewState (the same shape captureViewState() returns), so sheets serialize anywhere a saved view does.

sheets: {
sheets: [{ id: "data", name: "Data" }], // empty list → a synthesized Data tab
activeSheetId: "data",
onChange: (next) => persist(next), // add/rename/recolor/duplicate/delete/switch
onActiveSheetChange: (id) => setActive(id),
}

Two contract edges worth knowing:

  • Re-supplying the option (updateGridOptions({ sheets })) is sync-only — it updates the list and active highlight but never applies a view state. User tab clicks (and the keyboard chords) are what swap states; for a programmatic jump, call api.applyViewState(sheet.state) yourself.
  • A sheet supplied without a state adopts the grid's current state on its first activation, and gets captured like any other sheet when the user leaves it. Selection is not part of GridViewState, so switching sheets clears it.

Because all sheets share the row model, a cell edit made on any sheet is immediately visible everywhere: the pivot sheets re-derive from the same rows the Data sheet shows. Only the active sheet computes anything — inactive sheets are just stored state.