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.
- React
- Angular
- Core TypeScript
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,
}}
/>
<awb-grid
[rowData]="rows"
[columnDefs]="columns"
[pagination]="true"
[toolbar]="{ pivot: true }"
[sheets]="sheetsOptions()"
/>
readonly sheets = signal<GridSheet[]>([{ id: "data", name: "Data" }]);
readonly sheetsOptions = computed<SheetsOptions>(() => ({
sheets: this.sheets(),
onChange: (next) => this.sheets.set(next), // persist anywhere — the app owns the list
}));
const api = createGrid(host, {
rowData,
columnDefs,
pagination: true,
toolbar: { pivot: true },
// Supplying the option mounts the footer tab strip. A sheet is a live view
// state: switching tabs captures the sheet you leave and applies the next.
sheets: {
sheets: [{ id: "data", name: "Data" }],
onChange: (next) => save(next), // persist anywhere — the app owns the list
},
});
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.colorand is reported throughonChangelike 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
tablistwith 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 ownariaLabel).
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, callapi.applyViewState(sheet.state)yourself. - A sheet supplied without a
stateadopts 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 ofGridViewState, 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.