Number formats and cell styles
In Excel, format is a property of the cell, not the column. Two cells in
the same column can show $1,234.50 and 123450% from the same stored number.
@svgrid/enterprise ships that: an Excel format-string compiler, and a store
that keeps per-cell formatting alive through sorting.
import {
compileNumberFormat, createFormatStore, entryToStyle,
} from '@svgrid/enterprise/sheet'
This is different from the column-level
formatprop, which isIntl-based and applies to every cell in a column. Use that when a column has one meaning; use this when the user picks per cell.
Formatting a value
const money = compileNumberFormat('$#,##0.00;($#,##0.00)')
money.format(1234.5) // { text: '$1,234.50' }
money.format(-1234.5) // { text: '($1,234.50)' }
Compilation is cached per pattern, so a column of ten thousand cells sharing
one format parses it once. formatWithPattern(value, pattern) is the one-shot
convenience.
The pattern grammar
Sections
A pattern has up to four sections separated by ;:
positive ; negative ; zero ; text
With two, the second covers negatives and zero. With one, it covers everything.
A negative rendered by its own section uses its absolute value, because that section supplies the sign:
formatWithPattern(-1.5, '0.00;(0.00)') // '(1.50)' not '(-1.50)'
Digit placeholders
| Token | Meaning |
|---|---|
0 |
A digit, or a zero if there is none |
# |
A digit, or nothing |
? |
A digit, or a space (so decimal points line up) |
formatWithPattern(5, '000') // '005'
formatWithPattern(0, '#') // '' hides zeros
formatWithPattern(1.5, '0.0#') // '1.5' second decimal only when present
formatWithPattern(1.55, '0.0#') // '1.55'
Separators and scaling
, between placeholders groups thousands. , immediately before the decimal
point or at the end of the pattern divides by a thousand per comma:
formatWithPattern(1234567, '#,##0') // '1,234,567'
formatWithPattern(1500000, '0,,"M"') // '2M'
formatWithPattern(1500, '0.0,"k"') // '1.5k'
% multiplies by 100 and prints the sign. E+ switches to scientific, where
the placeholders after the E+ size the exponent rather than the mantissa:
formatWithPattern(0.425, '0.00%') // '42.50%'
formatWithPattern(12345, '0.00E+00') // '1.23E+04'
Colours and literals
[Red], [Blue], [Green], [Black], [White], [Cyan], [Magenta] and
[Yellow] set a colour rather than printing. It comes back on the result:
compileNumberFormat('0.00;[Red](0.00)').format(-5)
// { text: '(5.00)', color: '#ff0000' }
Text in quotes, or after a backslash, is emitted as-is. @ in the fourth
section is the text placeholder.
Conditions like [<100] are not supported; they are dropped rather than
printed.
Dates
A pattern containing date tokens is a date pattern.
| Token | Gives |
|---|---|
yyyy yy |
2026, 26 |
mmmm mmm mm m |
September, Sep, 09, 9 |
dddd ddd dd d |
Monday, Mon, 14, 14 |
hh h ss s |
Hours and seconds |
AM/PM |
Meridiem |
m and mm mean minutes after an hour token and months otherwise, so
hh:mm gives 15:05 rather than 15:09.
Numbers are read as Excel serial days against the 1899-12-30 epoch. That is correct from 1900-03-01 on; Excel itself is a day out below serial 61 because it counts a 1900-02-29 that never existed, and matching that bug exactly would break real dates.
Presets
FORMAT_PRESETS holds what Ctrl+Shift+1 through 6 apply: number, time,
date, currency, percent, scientific, plus general.
The per-cell store
const store = createFormatStore()
const lookup = {
rowIdAt: (i) => rows[i]?.id ?? null,
columnIdAt: (i) => FIELDS[i] ?? null,
}
store.set([[0, 2, 4, 3]], { numFmt: '$#,##0.00' }, lookup)
store.toggle([[0, 0, 0, 4]], 'bold', lookup)
store.get('r1', 'price') // { numFmt: '$#,##0.00' }
Ranges are [minRow, minCol, maxRow, maxCol] in display coordinates, the
same as cmd.ranges. The store converts them to row and column ids on the
way in, which is what makes formatting survive a sort: keying by display index
means sorting leaves the bold on whatever row now sits at that position.
| Method | Does |
|---|---|
set(rects, patch, at) |
Merge. A field set to undefined is removed. |
clear(rects, at) |
Remove every entry in the range. |
toggle(rects, field, at) |
Excel's rule: all-on turns off, mixed turns on. |
forgetRow(id) / forgetColumn(id) |
Call when one is deleted, or entries leak. |
serialize() / hydrate() |
The only persistence path. |
entryToStyle(entry) renders one to inline CSS for a cell renderer.
Wiring the shortcuts
Ctrl+B, Ctrl+Shift+4 and the rest write through whatever store you attach.
Until you attach one they decline, so the key falls through to the grid rather
than looking broken:
import { enableSheet, setFormatTarget } from '@svgrid/enterprise'
enableSheet()
setFormatTarget({ store, lookup, onChange: () => (version += 1) })
Ctrl+1 calls setFormatDialogHandler(fn) if you registered one. The shortcut
layer ships no dialog: what a Format Cells dialog should look like is a design
decision, not a keyboard one.
Exporting
Per-cell formats round-trip to xlsx through the exporter's existing
cellVisual hook, so a sheet formatted in the browser opens formatted in
Excel. See export.
See also
Related articles
- Conditional Formatting - Color Cells by Their Value - Four rule types, one prop - add heatmaps, data bars, icon sets, and threshold highlights to any SvGrid column without custom cell renderers.
- Progress and Percentage Bar Cells in SvGrid - Build in-cell progress bars in your Svelte 5 data grid - with color thresholds, accessible markup, and sorting that still works.
- A Fill Handle (Drag to Fill) in SvGrid - Build a working spreadsheet-style fill handle on top of SvGrid's cell selection and editing - pointer tracking, range highlighting, series fill, and undo/redo integration all covered.