Skip to content
Sheetwrite

Sheetwrite / Documentation

Configuration

Configure data ownership, rendering, interaction, theming, protection, and toolbar behavior.

Installation

Everything you pass to createGrid(host, opts) lives in GridOptions. This page lists every field with its type and default, the optional toolbar GridConfig, and the Grid instance API.

GridOptions

Partial example
type CreateGrid = (host: HTMLElement, opts: GridOptions) => Grid;
FieldTypeDefaultNotes
workbookWorkbook— (required)Sheets, columns, row counts, and the activeSheet id.
dataColumnarDataundefinedEager, in-memory, column-major values. Pass this or datasource.
datasourceDataSourceundefinedLazy, paged async source; rows fetched per visible window.
datasourceStorageDataSourceStorageOptions{ mode: "dense" }Use { mode: "paged", chunkRows?, cacheBytes? } for allocation-lazy datasource storage.
renderer"canvas" | "worker""canvas""worker" paints off-thread via OffscreenCanvas, falling back to canvas. See Worker rendering.
workerUrlstring | URLundefinedBrowser-fetchable module URL, e.g. "/sheetwrite/worker.js" after copying the package dist/; Vite can import @sheetwrite/core/worker?worker&url. See Worker rendering.
themePartial<Theme>undefinedOverrides merged over DEFAULT_THEME and any --sheetwrite-* CSS vars. See Styling.
readOnlybooleanfalseWhen true, all mutating interactions (edit, clear, fill, paste, restyle) are disabled and the host gets aria-readonly="true".
protectionResolverProtectionResolverundefinedHost callback for protected local mutations; no resolver means deny. Client-side UX policy only, never server authorization.
mutationPolicy"atomic" | "partial""atomic"Reject the whole local transaction on a denied protected operation, or apply allowed operations and report denied ones.
renderersRecord<string, CellRenderer>{}Custom cell renderers registered up front; reference one by name via Column.renderer. Also see defineCellRenderer.
overscannumber6Rows rendered above and below the viewport to absorb fast scrolls.
minColumnsnumberworkbook widthMinimum rendered/store column count, including empty spreadsheet padding columns.
configGridConfigundefinedPresence opts into the built-in toolbar (see below). Omit for no toolbar.

Framework adapters classify every GridOptions field centrally. workbook, data, datasource, datasourceStorage, protectionResolver, and mutationPolicy create an input-reset; renderer, workerUrl, and renderers create a renderer-reset. theme, readOnly, config, overscan, and minColumns update the existing grid live. Readiness includes the resulting generation and reset reason.

Framework adapters also accept wasmSource. Initialization is process-wide and first-source-wins: concurrent calls using the same source share one attempt, while a different source rejects until that attempt settles. If the winning attempt succeeds, every still-mounted adapter becomes ready even when its prop changed during the attempt. Changing wasmSource after readiness warns and keeps the live grid, selection, edits, generation, and ready-event count unchanged. A true initialization failure remains observable through onInitializationError and a later source can retry it.

Datasource pages

The cancellable request API owns one visible-window generation. Pages can carry authoritative formula source and style rather than only resolved scalars:

Partial example
import type { DataSource } from "@sheetwrite/core";
interface ApiRow {
label: string;
quantity: number;
price: number;
}
const datasource: DataSource = {
async getRows({ sheet, start, end, signal, revision }) {
const response = await fetch(
`/sheets/${encodeURIComponent(sheet)}?start=${start}&end=${end}`,
{ signal },
);
if (!response.ok) throw new Error(`Datasource request failed: ${response.status}`);
const records = (await response.json()) as ApiRow[];
return {
start,
revision: response.headers.get("etag") ?? revision,
rows: records.map((record, index) => ({
label: record.label,
quantity: record.quantity,
price: record.price,
total: {
value: {
kind: "formula",
src: `=B${start + index + 1}*C${start + index + 1}`,
},
style: { numberFormat: "$#,##0.00", bold: true },
},
})),
};
},
};

rows may contain scalars, CellValue objects, or { value: CellValue, style?: CellStyle } wrappers. Hydrated formulas, references, and styles do not emit user change events. Short pages mark only the returned rows loaded; malformed ranges emit datasource-error and remain retryable. Resetting or destroying the grid aborts outstanding requests, and a late page never overwrites a cell edited after that request began.

Dense storage is the default. { mode: "paged" } allocates power-of-two row chunks only for loaded or locally edited areas; cacheBytes bounds clean cached chunks, while dirty chunks remain pinned until acknowledgement. Full-sheet queries and exports report incomplete data until all required pages are loaded. Store.queryCapability(sheet) and getCellLoadState(addr) expose that state.

Serializable documents

Use WorkbookSnapshot plus validateWorkbookSnapshot() at persistence boundaries. schemaVersion: 1 rejects unsupported future schemas with structured errors. DocumentOp is the exhaustive reducer operation union, including metadata and sheet lifecycle operations. Session-only grid options such as renderer, readOnly, local zoom, selection, scroll, search, and temporary highlights never belong in a snapshot.

createGridFromSnapshot(host, snapshot, options) validates and hydrates every sheet before mounting. Hydration emits no change event or undo entry. grid.exportSnapshot() uses bulk sheet reads and returns deterministic sparse blocks. grid.applyRemoteOperations(operations) emits a change with source: "remote" while remaining outside local undo history and SyncCoordinator's outgoing queue. SyncCoordinator queues each non-empty local transaction as an immutable mutation record. Its serverVersion option is required and should come from the loaded snapshot. sendNext() and retry(id) are host-controlled; retries retain the original ID. subscribe(source) accepts a transport-neutral callback source and validates strict version order. Use MemoryPersistenceAdapter as an executable, server-sequenced reference—not as durable storage. Adapter methods accept AbortSignal; transport failures use PersistenceError, while version conflicts are typed commit responses that retain local work.

A CellRenderer paints (or returns a DOM node for) a single cell:

Partial example
interface CellRenderer {
canvas?(ctx: CanvasRenderingContext2D, c: CellPaintContext): void;
dom?(c: CellPaintContext): HTMLElement;
}

Custom renderers are not available under renderer: "worker" (functions can not be transferred to the worker).

GridConfig (toolbar)

Set config to show the built-in toolbar. Each flag toggles one control; all flags default to true when config is present. The one special case is toolbar: false, which suppresses the toolbar entirely.

Partial example
createGrid(host, { workbook, config: {} }); // toolbar with every control
createGrid(host, { workbook, config: { sort: false } }); // toolbar, no sort control
createGrid(host, { workbook, config: { toolbar: false } }); // no toolbar
createGrid(host, { workbook }); // no toolbar (config omitted)
FlagTypeDefaultControl
toolbarbooleantrue¹Master switch. false removes the toolbar.
boldbooleantrueBold toggle.
italicbooleantrueItalic toggle.
alignbooleantrueLeft / center / right alignment.
textColorbooleantrueText color picker.
fillColorbooleantrueFill (background) color picker.
borderbooleantrueBorder control.
clearFormatbooleantrueClear formatting.
mergebooleantrueMerge / unmerge selection.
sortbooleantrueSort the selected column.
exportbooleantrueCSV / XLSX export buttons.
undobooleantrueUndo / redo buttons (also bound to Ctrl+Z / Ctrl+Shift+Z).

¹ "Default true" means: when you supply a config object at all. With no config there is no toolbar.

The export flag always enables CSV. Its XLSX button requires an explicit optional installation and import "@sheetwrite/xlsx/register" before use; the framework packages do not install an XLSX backend. Direct grid.exportXlsx(...) calls reject on failure; built-in toolbar and context-menu actions report the same failure through one export-error event.

Every control acts on the current selection — see Interaction.

Feature flags

Three GridConfig fields enable behavior that lives outside the toolbar row:

FieldTypeDefaultEffect
findbooleantrueBuilt-in Ctrl+F search box, next/previous controls, and live match count.
contextMenuboolean | ContextMenuItemstrueBuilt-in right-click menu, disabled menu, static readonly rows, or a context-aware row factory.
iconsPartial<Record<ToolbarActionName, ToolbarIcon>>undefinedBuilt-in toolbar icon overrides. Strings render as text; a DOM Node or () => Node supports SVG/HTML without innerHTML.

Context-menu items

The default menu contains copy, cut, paste, clear contents, row insert/delete/hide/show/auto-fit, column insert/delete/hide/show/auto-fit, clear column filter, merge, and unmerge actions, with separators between groups. exportCsv and exportXlsx are also valid built-in actions in a custom list.

ContextMenuItems is either a readonly ContextMenuItem[] or a factory evaluated for each ContextMenuContext. An item may set a stable id, built-in action, visible label, shortcut hint, and static or context-aware visible/disabled policy. The lean context carries only cell, clientX, and clientY. onClick(grid, cell) overrides action; hidden separators are normalized.

Context-aware bundled menu
const config = {
contextMenu: (context) => [
{ id: "copy", action: "copy", label: "Copy value", shortcut: "Ctrl+C" },
{ action: "separator" },
{
id: "inspect",
label: "Inspect cell",
visible: context.cell !== null,
disabled: ({ cell }) => cell === null,
onClick(grid, cell) {
if (cell !== null) console.log(grid.store.getCell(cell));
},
},
{ action: "exportCsv", label: "Download CSV" },
],
} satisfies GridConfig;

For fully host-owned UI, set contextMenu: false, listen for the DOM contextmenu event on the host, call grid.getCellAtPoint(event.clientX, event.clientY), update selection, and invoke grid.actions from the host menu. Sheetwrite does not prescribe the host's event lifecycle:

Host-owned context menu
host.addEventListener("contextmenu", (event) => {
event.preventDefault();
const cell = grid.getCellAtPoint(event.clientX, event.clientY);
if (cell !== null) grid.setSelection({ kind: "cell", addr: cell });
host.dispatchEvent(
new CustomEvent("sheetwrite:context-menu", {
detail: { cell, x: event.clientX, y: event.clientY, actions: grid.actions },
}),
);
});

See Interaction → Context menu, Styling, and the generated ContextMenuContext, ContextMenuItems, ContextMenuItem, and Grid.getCellAtPoint contracts.

Undo/redo and find are keyboard-driven and work without the toolbar: Ctrl+Z undoes the last edit, Ctrl+Shift+Z redoes it, and Ctrl+F opens the find widget (unless find: false).

Grid instance

createGrid returns an imperative handle:

Partial example
interface Grid {
readonly store: Store;
readonly actions: GridActions;
setActiveSheet(id: SheetId): void;
scrollToCell(addr: CellAddress): void;
getSelection(): Selection | null;
setSelection(sel: Selection | null): void;
setTheme(theme: Partial<Theme>): void;
defineCellRenderer(name: string, renderer: CellRenderer): void;
aggregate(col: number, op: AggregateOp): number;
sortBy(col: number, ascending?: boolean): void;
sortByMulti(keys: readonly SortKey[]): void;
filterBy(col: number, needle: string): void;
setColumnFilter(col: number, filter: ColumnFilter | null): void;
getColumnFilters(): ReadonlyMap<number, ColumnFilter>;
distinctValues(col: number, limit?: number): CellScalar[];
hideRows(rows: readonly number[]): void;
showRows(rows?: readonly number[]): void;
hiddenRows(): readonly number[];
hideColumns(cols?: readonly number[]): void;
showColumns(cols?: readonly number[]): void;
hiddenColumns(): readonly number[];
groupRows(start: number, end: number): void;
ungroupRows(start: number, end: number): void;
setGroupCollapsed(start: number, collapsed: boolean): void;
rowGroups(): readonly RowGroup[];
clearView(): void;
undo(): void;
redo(): void;
exportCsv(filename: string): void;
exportXlsx(filename: string): Promise<void>;
search(query: string, opts?: SearchOptions): SearchResult;
findNext(): SearchResult;
findPrev(): SearchResult;
clearSearch(): void;
replaceCurrent(replacement: string): SearchResult;
replaceAll(replacement: string): ReplaceResult;
insertRows(at: number, count?: number): void;
removeRows(at: number, count?: number): void;
insertColumns(at: number, count?: number): void;
removeColumns(at: number, count?: number): void;
highlightCells(ranges: readonly HighlightRange[] | null, color?: string): void;
styleRange(range: Range, style: Partial<CellStyle> | null): void;
setValidationRule(rule: DataValidationRule): ApplyTransactionResult;
removeValidationRule(id: string): ApplyTransactionResult;
setProtectedRange(range: ProtectedRange): ApplyTransactionResult;
removeProtectedRange(id: string): ApplyTransactionResult;
setProtectionResolver(resolver?: ProtectionResolver, mode?: MutationPolicyMode): void;
setNote(addr: CellAddress, text: string | null): ApplyTransactionResult;
getNote(addr: CellAddress): string | null;
beginEdit(row: number, col: number, initial?: string, selectAll?: boolean): void;
dataEdge(row: number, col: number, dRow: number, dCol: number): number | null;
setRowHeight(row: number, height: number): void;
setColumnWidth(col: number, width: number): void;
autoFitRows(range?: Range): void;
autoFitColumns(cols?: readonly number[]): void;
setFrozen(rows: number, cols?: number): void;
setZoom(zoom: number): void;
getZoom(): number;
on<E extends keyof GridEvents>(evt: E, fn: (e: GridEvents[E]) => void): () => void;
refresh(): void;
destroy(): void;
}
MethodPurpose
storeThe underlying Store — apply transactions, read cells, subscribe.
actionsImperative action surface (toggleBold(), merge(), undo(), exportCsv(), …) the toolbar and context menu bind to — use it to wire custom controls.
setActiveSheet(id)Switch the visible sheet.
scrollToCell(addr)Scroll a cell into view.
getSelection() / setSelection(sel)Read or set the current Selection (null clears it).
setTheme(partial)Merge a partial theme and repaint.
defineCellRenderer(name, r)Register a custom renderer after construction.
aggregate(col, op)Column aggregate; see Data operations.
sortBy / sortByMultiNon-mutating display sorts; see Data operations.
filterBy / setColumnFilter / getColumnFiltersColumn filters that compose with sort, hidden rows, and row groups.
distinctValues(col, limit?)First-seen distinct values for building filter menus.
hideRows / showRows / hiddenRowsExplicit row visibility separate from sort/filter state.
hideColumns / showColumns / hiddenColumnsBulk-safe persisted column visibility; omitted arguments target the focused column for hide and every column for show.
groupRows / ungroupRows / setGroupCollapsed / rowGroupsInclusive data-row groups with collapse state.
clearView()Clears sort/filter state; hidden rows and row groups remain.
setFrozen(rows, cols?)Pin leading view rows/columns while the body scrolls.
setZoom(z) / getZoom()Scale grid content between 0.5 and 2 without mutating workbook base sizes.
undo() / redo()Undo or redo the last recorded cell edit (also bound to Ctrl+Z / Ctrl+Shift+Z).
exportCsv / exportXlsxDownload the active data; see Data operations.
search(query, opts?)Find matching cells; highlights them, emits search, returns a SearchResult.
findNext() / findPrev()Step the active match forward / backward and scroll it into view.
clearSearch()Drop the current search and clear its highlights.
replaceCurrent() / replaceAll()Replace literal text/number matches through undoable transactions.
insertRows / removeRows / insertColumns / removeColumnsStructural edits through undoable patches.
highlightCells(ranges, color?)Highlight arbitrary ranges (null clears); color overrides the theme highlight.
styleRange(range, style)Merge or clear store-backed cell styles across a range.
setValidationRule / removeValidationRuleAdd, replace, or remove a serializable range validation rule. List and checkbox rules get accessible editors.
setProtectedRange / removeProtectedRange / setProtectionResolverDefine protected-range metadata and host-owned local permission policy. This is not server authorization.
setNote / getNoteSet, clear, or read a serializable plain-text cell note.
beginEdit(row, col, initial?, selectAll?)Open the inline editor at a view cell.
dataEdge(row, col, dRow, dCol)Ctrl+Arrow-style data-run jump target; vertical movement is view-aware under sort/filter.
setRowHeight(row, h) / setColumnWidth(col, w)Geometry APIs; row height is view-indexed and persists against the underlying data row.
autoFitRows(range?) / autoFitColumns(cols?)Explicit, undoable geometry fitting from bulk reads; auto-fit never runs during paint.
on(evt, fn)Subscribe to an event; returns an unsubscribe function.
refresh()Force a re-render (e.g. after mutating the workbook directly).
destroy()Tear down listeners, DOM, and ARIA attributes.

grid.search(query, opts?) scans cells, highlights every match, scrolls the first match into view, and emits a search event. findNext() / findPrev() move the active match; clearSearch() clears the highlights. The built-in Ctrl+F find widget (gated by config.find, on by default) drives this same API.

Partial example
interface SearchOptions {
matchCase?: boolean; // case-sensitive match (default false)
wholeCell?: boolean; // match the whole cell, not a substring (default false)
sheet?: SheetId; // restrict to one sheet (default: the active sheet)
columns?: number[]; // restrict to these column indices (default: all)
}
interface SearchResult {
query: string;
matches: CellAddress[]; // matching cells, in row-major order
active: number; // index of the active match, or -1 when there are none
}
Partial example
const result = grid.search("error");
console.log(`${result.matches.length} match(es)`);
grid.findNext(); // advance the active match and scroll to it
grid.clearSearch(); // remove the highlights when done

highlightCells(ranges, color?) highlights arbitrary ranges independently of search (pass null to clear); color overrides the theme highlight color.

Events

grid.on(evt, fn) returns an off() you should call to unsubscribe.

EventPayload
change{ transaction; changes; dirty; commitReason; source: "local" | "remote"; epoch? }
selection{ selection: Selection | null }
scroll{ scrollTop: number; firstRow: number; lastRow: number }
edit-begin{ addr: CellAddress }
edit-commit{ addr: CellAddress; value: CellValue }
searchSearchResult{ query: string; matches: CellAddress[]; active: number }
Partial example
const sync = new SyncCoordinator(grid, adapter, {
documentId: "products",
serverVersion: loadedSnapshot.version ?? 0,
});
const off = sync.on((event) => {
if (event.type === "conflict") showConflict(event.response);
if (event.type === "reload-required") requestFreshSnapshot();
});
saveButton.onclick = () => void sync.sendNext();
// later
off();
sync.destroy();