Sheetwrite paints cells on a canvas, so styling has two layers: the Theme
(colors, fonts, and geometry the renderer uses) and CellStyle (per-cell and
per-column formatting). A small stylesheet provides the host container chrome and
CSS custom properties.
Theme
The Theme controls the canvas paint. Pass a Partial<Theme> as the theme
option, or call grid.setTheme(partial) at runtime; either is merged over the
defaults.
Partial example
interfaceThemeinterface Theme
interface ThemeinterfaceThemeinterfaceThemeinterfaceThemeinterfaceThemeResolved canvas colors, typography, and geometry used for painting.API reference → {
fontTheme.font: stringinterfaceTheme {font:string;}interfaceTheme {font:string;}Canvas font shorthand used for unstyled cells.API reference →:string;
bgTheme.bg: stringinterfaceTheme {bg:string;}interfaceTheme {bg:string;}CSS color painted behind body cells.API reference →:string;
fgTheme.fg: stringinterfaceTheme {fg:string;}interfaceTheme {fg:string;}CSS color used for unstyled cell text.API reference →:string;
gridLineTheme.gridLine: stringinterfaceTheme {gridLine:string;}interfaceTheme {gridLine:string;}CSS color used for cell grid lines.API reference →:string;
headerBgTheme.headerBg: stringinterfaceTheme {headerBg:string;}interfaceTheme {headerBg:string;}CSS color painted behind column and row headers.API reference →:string;
headerFgTheme.headerFg: stringinterfaceTheme {headerFg:string;}interfaceTheme {headerFg:string;}CSS color used for column letters and row numbers.API reference →:string;
selectionTheme.selection: stringinterfaceTheme {selection:string;}interfaceTheme {selection:string;}CSS color painted over the selected region.API reference →:string;
selectionBorderTheme.selectionBorder: stringinterfaceTheme {selectionBorder:string;}interfaceTheme {selectionBorder:string;}CSS color used for the active selection outline.API reference →:string;
rowHeaderWidthTheme.rowHeaderWidth: numberinterfaceTheme {rowHeaderWidth:number;}interfaceTheme {rowHeaderWidth:number;}Width of the left row-number gutter (0 hides it).API reference →:number; // 0 hides the left row-number gutter
searchMatchTheme.searchMatch: stringinterfaceTheme {searchMatch:string;}interfaceTheme {searchMatch:string;}Fill behind a search match.API reference →:string; // fill behind a search match
searchActiveMatchTheme.searchActiveMatch: stringinterfaceTheme {searchActiveMatch:string;}interfaceTheme {searchActiveMatch:string;}Fill/outline for the active (current) search match.API reference →:string; // fill/outline for the active (current) match
highlightTheme.highlight: stringinterfaceTheme {highlight:string;}interfaceTheme {highlight:string;}Fill for cells highlighted via Grid.highlightCells.API reference →:string; // fill for cells highlighted via Grid.highlightCells
Note: the shipped styles.css declares different values for the search/highlight
CSS custom properties (#fff47580, #fbbc04, #e8f0fe99); on a host that
imports the stylesheet those win over DEFAULT_THEME per the resolution order
below.
Override the search-match colors at runtime via setTheme:
Partial example
gridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →.setThemeGrid.setTheme(theme: Partial<Theme>): voidGrid.setTheme(theme: Partial<Theme>): voidGrid.setTheme(theme: Partial<Theme>): voidImperative patch: merge theme into the accumulated base theme.API reference →({ searchMatchsearchMatch?: string | undefinedsearchMatch?: string |undefinedsearchMatch?: string |undefinedFill behind a search match.API reference →: "#fff47580", searchActiveMatchsearchActiveMatch?: string | undefinedsearchActiveMatch?: string |undefinedsearchActiveMatch?: string |undefinedFill/outline for the active (current) search match.API reference →: "#fbbc04" });
Resolution order
The effective theme is computed once at construction and again on setTheme:
resolveThemeFromCss(host) reads CSS custom properties from the host's computed
style into a partial theme, and opts.theme wins last.
Two runtime methods with different semantics:
grid.setTheme(partial) — imperative patch: merges into the accumulated
base theme and repaints. Use for incremental tweaks.
grid.replaceTheme(partial | undefined) — option-level replacement:
re-runs the construction-time resolution above with the new value;
undefined restores the CSS-variable/default resolution. The framework
adapters call this for their theme prop, so the prop is authoritative:
removing it (or dropping a field) restores defaults instead of leaving stale
merged values behind.
grid.getEffectiveTheme() returns the effective (post-zoom) theme currently
painting.
Tailwind works through the same seam: set --sheetwrite-* custom properties on
the host with arbitrary-property utilities (e.g.
class="[--sheetwrite-bg:#0b0b0c] [--sheetwrite-fg:#e7e7e7]") or a @layer
rule; construction and replaceTheme(undefined) both pick them up from the
computed style.
Partial example
constDARK:Partialtype Partial<T> = { [P in keyof T]?: T[P] | undefined; }typePartial<T> = { [PinkeyofT]?:T[P] |undefined };typePartial<T> = {[PinkeyofT]?:T[P] |undefined;};Make all properties in T optional<Themeinterface ThemeinterfaceThemeinterfaceThemeResolved canvas colors, typography, and geometry used for painting.API reference →> = {
bgbg?: string | undefinedbg?: string |undefinedbg?: string |undefinedCSS color painted behind body cells.API reference →: "#0b0b0c",
fgfg?: string | undefinedfg?: string |undefinedfg?: string |undefinedCSS color used for unstyled cell text.API reference →: "#e7e7e7",
gridLinegridLine?: string | undefinedgridLine?: string |undefinedgridLine?: string |undefinedCSS color used for cell grid lines.API reference →: "#26262a",
headerBgheaderBg?: string | undefinedheaderBg?: string |undefinedheaderBg?: string |undefinedCSS color painted behind column and row headers.API reference →: "#1a160f",
headerFgheaderFg?: string | undefinedheaderFg?: string |undefinedheaderFg?: string |undefinedCSS color used for column letters and row numbers.API reference →: "#e8c98a",
selectionselection?: string | undefinedselection?: string |undefinedselection?: string |undefinedCSS color painted over the selected region.API reference →: "#e8c98a22",
selectionBorderselectionBorder?: string | undefinedselectionBorder?: string |undefinedselectionBorder?: string |undefinedCSS color used for the active selection outline.API reference →: "#e8c98a",
};
gridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →.setThemeGrid.setTheme(theme: Partial<Theme>): voidGrid.setTheme(theme: Partial<Theme>): voidGrid.setTheme(theme: Partial<Theme>): voidImperative patch: merge theme into the accumulated base theme.API reference →(DARKconst DARK: Partial<Theme>constDARK:Partial<Theme>constDARK:Partial<Theme>);
CSS custom properties & the stylesheet
Import the stylesheet once. It styles the host container (the grid adds the
.sheetwrite class to your host element), declares the --sheetwrite-* defaults,
and ships a dark variant under [data-theme="dark"] or a .dark ancestor class
(the Tailwind convention):
Partial example
import"@sheetwrite/core/styles.css";
resolveThemeFromCss maps these custom properties onto Theme fields:
CSS custom property
Theme field
--sheetwrite-bg
bg
--sheetwrite-fg
fg
--sheetwrite-grid-line
gridLine
--sheetwrite-header-bg
headerBg
--sheetwrite-header-fg
headerFg
--sheetwrite-selection
selection
--sheetwrite-selection-border
selectionBorder
--sheetwrite-row-height
rowHeight (parsed as a number)
--sheetwrite-search-match
searchMatch
--sheetwrite-search-active
searchActiveMatch
--sheetwrite-highlight
highlight
--sheetwrite-font
font (a / line-height segment is stripped for the canvas)
headerHeight and rowHeaderWidth have no CSS variable — set them through the
theme option. An explicit --sheetwrite-* value set on the .sheetwrite
element itself still wins over either dark selector (same-element beats
ancestor).
Widget and context-menu styling
The stylesheet exposes widget variables in addition to the canvas theme
mapping:
CSS custom property
Widget use
Fallback
--sheetwrite-widget-border
Borders and separators for toolbar/find/menu chrome
--sheetwrite-grid-line
--sheetwrite-menu-hover
Context-menu item hover background
--sheetwrite-grid-line
--sheetwrite-menu-disabled-opacity
Disabled context-menu row opacity
0.5
The bundled menu uses the public .sheetwrite-context-menu container,
.sheetwrite-context-menu-item row, .sheetwrite-context-menu-item-disabled
state, .sheetwrite-context-menu-shortcut hint, and
.sheetwrite-context-menu-sep separator classes. Scope overrides beneath the
grid host rather than replacing positioning, visibility, or disabled behavior:
Context menu theme hooks
.sheetwrite {
--sheetwrite-widget-border: #334155;
--sheetwrite-menu-hover: #1e293b;
--sheetwrite-menu-disabled-opacity: 0.45;
}
.sheetwrite.sheetwrite-context-menu-item {
font-weight: 500;
}
The recommended runtime-theming pattern drives both layers together: toggle
data-theme on a wrapper so the surrounding CSS-variable chrome flips, and
call setTheme so the canvas repaints.
Partial example
darkBtnconst darkBtn: HTMLButtonElementconstdarkBtn:HTMLButtonElementconstdarkBtn:HTMLButtonElement.addEventListenerHTMLButtonElement.addEventListener<"click">(type: "click", listener: (this: HTMLButtonElement, ev: PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this:HTMLButtonElement, ev:PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)HTMLButtonElement.addEventListener<"click">(type: "click", listener: (this:HTMLButtonElement, ev:PointerEvent) => any, options?: boolean | AddEventListenerOptions): void (+1 overload)The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target. MDN Reference Adds a new handler for the type event. Any given listener is added only once per type and per capture option value. If the once option is true, the listener is removed after the next time a type event is dispatched. The capture option is not used by Node.js in any functional way other than tracking registered event listeners per the EventTarget specification. Specifically, the capture option is used as part of the key when registering a listener. Any individual listener may be added once with capture = false, and once with capture = true. The addEventListener() method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target. MDN Reference Adds a new handler for the type event. Any given listener is added only once per type and per capture option value. If the once option is true, the listener is removed after the next time a type event is dispatched. The capture option is not used by Node.js in any functional way other than tracking registered event listeners per the EventTarget specification. Specifically, the capture option is used as part of the key when registering a listener. Any individual listener may be added once with capture = false, and once with capture = true.("click", () => {
gridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →.setThemeGrid.setTheme(theme: Partial<Theme>): voidGrid.setTheme(theme: Partial<Theme>): voidGrid.setTheme(theme: Partial<Theme>): voidImperative patch: merge theme into the accumulated base theme.API reference →(DARKconst DARK: Partial<Theme>constDARK:Partial<Theme>constDARK:Partial<Theme>);
stageconst stage: HTMLElementconststage:HTMLElementconststage:HTMLElement.setAttributeElement.setAttribute(qualifiedName: string, value: string): voidElement.setAttribute(qualifiedName: string, value: string): voidElement.setAttribute(qualifiedName: string, value: string): voidThe setAttribute() method of the Element interface sets the value of an attribute on the specified element. MDN Reference("data-theme", "dark");
});
CellStyle
CellStyle formats individual cells. It appears in three places:
as the style on a set patch — formatting for that one specific cell;
as Column.cellStyle — a column-wide default the renderer paints under every
body cell of the column;
as Column.headerStyle — the style applied to that column's header cell.
Column.cellStyle is a default, not a lock: a per-cell set style overrides the
column default for that cell, so a cell paints with its column's cellStyle until
its own patch supplies styling.
Partial example
interfaceCellStyleinterface CellStyle
interface CellStyleinterfaceCellStyleinterfaceCellStyleinterfaceCellStyleinterfaceCellStyleSerializable formatting applied to a cell or used as a column default.API reference → {
boldCellStyle.bold?: boolean | undefinedinterfaceCellStyle {bold?:boolean|undefined;}interfaceCellStyle {bold?:boolean|undefined;}Uses the bold variant of the theme font.API reference →?:boolean;
italicCellStyle.italic?: boolean | undefinedinterfaceCellStyle {italic?:boolean|undefined;}interfaceCellStyle {italic?:boolean|undefined;}Uses the italic variant of the theme font.API reference →?:boolean;
underlineCellStyle.underline?: boolean | undefinedinterfaceCellStyle {underline?:boolean|undefined;}interfaceCellStyle {underline?:boolean|undefined;}Draws a line beneath each rendered text run.API reference →?:boolean;
strikethroughCellStyle.strikethrough?: boolean | undefinedinterfaceCellStyle {strikethrough?:boolean|undefined;}interfaceCellStyle {strikethrough?:boolean|undefined;}Draws a line through each rendered text run.API reference →?:boolean;
fontSizeCellStyle.fontSize?: number | undefinedinterfaceCellStyle {fontSize?:number|undefined;}interfaceCellStyle {fontSize?:number|undefined;}Font size in unzoomed CSS pixels; zoom is applied during painting.API reference →?:number;
colorCellStyle.color?: string | undefinedinterfaceCellStyle {color?:string|undefined;}interfaceCellStyle {color?:string|undefined;}hex color, e.g. "#111111"API reference →?:string; // hex text color, e.g. "#111111"
backgroundColorCellStyle.backgroundColor?: string | undefinedinterfaceCellStyle {backgroundColor?:string|undefined;}interfaceCellStyle {backgroundColor?:string|undefined;}hex color, e.g. "#ffffff"API reference →?:string; // hex fill, e.g. "#ffffff"
alignCellStyle.align?: "left" | "center" | "right" | undefinedinterfaceCellStyle {align?:"left"|"center"|"right"|undefined;}interfaceCellStyle {align?:|"left"|"center"|"right"|undefined;}Horizontal placement of cell text within its column.API reference →?:"left"|"center"|"right";
wrapCellStyle.wrap?: boolean | undefinedinterfaceCellStyle {wrap?:boolean|undefined;}interfaceCellStyle {wrap?:boolean|undefined;}Wraps text within the cell width; row auto-fit accounts for the resulting line count.API reference →?:boolean;
borderCellStyle.border?: CellBorders | undefinedinterfaceCellStyle {border?:CellBorders|undefined;}interfaceCellStyle {border?:CellBorders|undefined;}Border overrides for the cell's individual sides.API reference →?:CellBordersinterface CellBordersinterfaceCellBordersinterfaceCellBordersPer-side borders; all applies to any side not given its own border.API reference →;
}
Apply a style to a cell via a transaction (a set replaces value + style, so pass
the current value back when only restyling):
Partial example
gridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →.storeGrid.store: StoreinterfaceGrid {store:Store;}interfaceGrid {store:Store;}.applyTransactionStore.applyTransaction(tx: Transaction, options?: TransactionApplicationOptions): ApplyTransactionResultStore.applyTransaction(tx: Transaction, options?: TransactionApplicationOptions): ApplyTransactionResultStore.applyTransaction(tx: Transaction, options?: TransactionApplicationOptions): ApplyTransactionResultApply a low-level storage transaction. This bypasses Grid read-only checks and Grid undo/redo history. Use Grid.applyTransaction for normal host-driven edits. Queued and flushed at a barrier — never reentrant.API reference →({
patchesTransaction.patches: DocumentOp[]interfaceTransaction {patches:DocumentOp[];}interfaceTransaction {patches:DocumentOp[];}Ordered document operations submitted as one store commit.API reference →: [
{
op: "set",
addraddr: CellAddresslet addr:CellAddresslet addr:CellAddress: { sheetCellAddress.sheet: stringinterfaceCellAddress {sheet:string;}interfaceCellAddress {sheet:string;}Restrict to a sheet (defaults to the active sheet).API reference →: "sheet1", rowCellAddress.row: numberinterfaceCellAddress {row:number;}interfaceCellAddress {row:number;}: 0, colCellAddress.col: numberinterfaceCellAddress {col:number;}interfaceCellAddress {col:number;}: 0 },
stylestyle?: CellStyle | undefinedstyle?: CellStyle |undefinedstyle?: CellStyle |undefinedHost styles merged before adapter sizing styles.API reference →: { boldCellStyle.bold?: boolean | undefinedinterfaceCellStyle {bold?:boolean|undefined;}interfaceCellStyle {bold?:boolean|undefined;}Uses the bold variant of the theme font.API reference →: true, alignCellStyle.align?: CellAlign | undefinedinterfaceCellStyle {align?:CellAlign|undefined;}interfaceCellStyle {align?:CellAlign|undefined;}Horizontal placement of cell text within its column.API reference →: "center", backgroundColorCellStyle.backgroundColor?: string | undefinedinterfaceCellStyle {backgroundColor?:string|undefined;}interfaceCellStyle {backgroundColor?:string|undefined;}hex color, e.g. "#ffffff"API reference →: "#eef1f5" },
},
],
});
Borders
Borders are per side. CellBorders.all applies to any side not given its own
border:
Partial example
interfaceCellBordersinterface CellBorders
interface CellBordersinterfaceCellBordersinterfaceCellBordersinterfaceCellBordersinterfaceCellBordersPer-side borders; all applies to any side not given its own border.API reference → {
conststyle:CellStyleinterface CellStyleinterfaceCellStyleinterfaceCellStyleSerializable formatting applied to a cell or used as a column default.API reference →= {
borderCellStyle.border?: CellBorders | undefinedinterfaceCellStyle {border?:CellBorders|undefined;}interfaceCellStyle {border?:CellBorders|undefined;}Border overrides for the cell's individual sides.API reference →: {
A numeric/date column can carry an Excel-style numberFormat code and an explicit
BCP 47 numberLocale. The same UTC serial value and locale are used by the
canvas renderer, auto-fit measurement, and exported formatter:
Partial example
constcolumns:Columninterface ColumninterfaceColumninterfaceColumnSchema and default presentation for one workbook column.API reference →[] = [
{
keyColumn.key: stringinterfaceColumn {key:string;}interfaceColumn {key:string;}Non-empty key, unique within the sheet, used to map input and datasource values.API reference →: "amount",
headerColumn.header: stringinterfaceColumn {header:string;}interfaceColumn {header:string;}Schema label written by table exports; the canvas header displays positional column letters.API reference →: "Amount",
typeColumn.type: CellFormatinterfaceColumn {type:CellFormat;}interfaceColumn {type:CellFormat;}Controls cell input parsing and default value formatting for this column.API reference →: "number",
numberLocaleColumn.numberLocale?: string | undefinedinterfaceColumn {numberLocale?:string|undefined;}interfaceColumn {numberLocale?:string|undefined;}Explicit BCP 47 locale for separators; omitted keeps the deterministic default.API reference →: "de-DE",
},
{ keyColumn.key: stringinterfaceColumn {key:string;}interfaceColumn {key:string;}Non-empty key, unique within the sheet, used to map input and datasource values.API reference →: "day", headerColumn.header: stringinterfaceColumn {header:string;}interfaceColumn {header:string;}Schema label written by table exports; the canvas header displays positional column letters.API reference →: "Day", widthColumn.width: numberinterfaceColumn {width:number;}interfaceColumn {width:number;}Unzoomed column width in CSS pixels.API reference →: 120, typeColumn.type: CellFormatinterfaceColumn {type:CellFormat;}interfaceColumn {type:CellFormat;}Controls cell input parsing and default value formatting for this column.API reference →: "date", numberFormatColumn.numberFormat?: string | undefinedinterfaceColumn {numberFormat?:string|undefined;}interfaceColumn {numberFormat?:string|undefined;}Excel number-format code, e.g. "#,##0.00"API reference →: "yyyy-mm-dd" },
];
formatNumber(value, code?, locale?) is exported from @sheetwrite/core. It is
a cached renderer for the supported Excel-style families:
Code
Example output
(none)
1234.5 → locale-default 1,234.5
0
1234.5 → 1235
0.00
1234.5 → 1234.50
#,##0.00
1234.5 → 1,234.50
0%
0.5 → 50%
$#,##0.00
1234.5 → $1,234.50
0.00E+00
12345 → 1.23E+04
yyyy-mm-dd
serial 45351 → 2024-02-29
mmm d, yyyy h:mm:ss AM/PM
UTC serial → Jul 4, 2024 3:06:07 PM
0.00;[Red](0.00);"none";"value: "@
positive / negative / zero / text sections
Numeric formats support fixed decimals, optional grouping, percent scaling,
scientific exponents with explicit width, quoted/escaped literals, and currency
prefixes/suffixes. Up to four semicolon sections select positive, negative, zero,
and text output; color directives such as [Red] are accepted as formatting
metadata but do not change canvas text color.
Date/time formats support numeric and named month/day fields, 12/24-hour time,
minutes, seconds, and AM/PM. Month/minute disambiguation follows neighboring time
tokens. UTC fields make date rendering deterministic across host timezones.
Locale-specific separators come from numberLocale, not ambient browser locale.
Fractions, elapsed-time bracket tokens, and fractional seconds are not supported.
Formula errors are explicit sentinels such as #DIV/0! and #CYCLE!; they are
not non-finite numbers hidden by the formatter. See
Formulas.
Native boolean values use the normal cell style model, render as uppercase
TRUE/FALSE, and default to centered alignment unless CellStyle.align
overrides it.
See also
Configuration for theme, renderers, and the toolbar color controls.
Concepts for the styled row-0 field-header pattern.