← All @molecule/* packages · App templates
@molecule/app-spreadsheet-grid-reactFeature · spreadsheet-grid · App (browser) · v1.0.1 · Apache-2.0
High-performance virtualized spreadsheet cell grid with frozen rows/columns, range selection, copy/paste as TSV, and in-cell editing — pairs with @molecule/api-formula-engine
npm install @molecule/app-spreadsheet-grid-react@molecule/app-spreadsheet-grid-react is a ready-made spreadsheet-grid feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.
import { useState } from 'react'
import { SpreadsheetGrid, type CellMap } from '@molecule/app-spreadsheet-grid-react'
function Sheet() {
const [cells, setCells] = useState<CellMap>(new Map())
const [selection, setSelection] = useState({ r1: 0, c1: 0, r2: 0, c2: 0 })
return (
<SpreadsheetGrid
rows={1000}
columns={26}
cells={cells}
onCellChange={(ref, value) => {
setCells((prev) => {
const next = new Map(prev)
if (value === null) next.delete(ref)
else next.set(ref, value)
return next
})
}}
selection={selection}
onSelectionChange={setSelection}
frozenRows={1}
frozenCols={1}
/>
)
}Works with: @molecule/app-i18n, @molecule/app-react, @molecule/app-ui
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
High-performance virtualized spreadsheet cell grid for React.
Exports:
<SpreadsheetGrid> — the grid component (virtualized cells, frozen
rows/columns, range selection, copy/paste as TSV, in-cell editing).SpreadsheetGridProps, SpreadsheetSelection, CellMap,
CellRef, CellValue.cellRef(), columnLetter(), normalizeSelection(),
isInSelection(), formatCellValue(), parseClipboardTsv(),
serializeSelectionTsv(), computeVisibleRange().Pairs with @molecule/api-formula-engine for formula evaluation —
pass evaluated cells plus an optional renderCell to display
formula results.
import { useState } from 'react'
import { SpreadsheetGrid, type CellMap } from '@molecule/app-spreadsheet-grid-react'
function Sheet() {
const [cells, setCells] = useState<CellMap>(new Map())
const [selection, setSelection] = useState({ r1: 0, c1: 0, r2: 0, c2: 0 })
return (
<SpreadsheetGrid
rows={1000}
columns={26}
cells={cells}
onCellChange={(ref, value) => {
setCells((prev) => {
const next = new Map(prev)
if (value === null) next.delete(ref)
else next.set(ref, value)
return next
})
}}
selection={selection}
onSelectionChange={setSelection}
frozenRows={1}
frozenCols={1}
/>
)
}
feature
npm install @molecule/app-spreadsheet-grid-react @molecule/app-i18n @molecule/app-react @molecule/app-ui react
npm install -D @types/react
SpreadsheetGridPropsProps for <SpreadsheetGrid>.
interface SpreadsheetGridProps {
/** Total number of rows in the grid. */
rows: number
/** Total number of columns in the grid. */
columns: number
/** Sparse cell-data map. Cells not present render as empty. */
cells: CellMap
/** Called when the user commits an edit (Enter, Tab, or focus loss). */
onCellChange: (ref: CellRef, value: CellValue) => void
/** Current selection. */
selection: SpreadsheetSelection
/** Called when the user changes the selection (click, drag, Shift+click). */
onSelectionChange: (selection: SpreadsheetSelection) => void
/** Number of rows frozen at the top (default `0`). */
frozenRows?: number
/** Number of columns frozen on the left (default `0`). */
frozenCols?: number
/** Per-cell width in pixels (default `96`). */
cellWidth?: number
/** Per-cell height in pixels (default `28`). */
cellHeight?: number
/** Width of the row-number gutter on the left (default `48`). */
gutterWidth?: number
/** Height of the column-letter header (default `28`). */
headerHeight?: number
/** Visible viewport width in pixels (default `720`). */
viewportWidth?: number
/** Visible viewport height in pixels (default `360`). */
viewportHeight?: number
/** `data-mol-id` for AI-agent selectors. */
dataMolId?: string
/**
* Optional cell renderer. Receives the cached display string plus the
* raw value. Defaults to plain text. Use to render formula results
* (`'=A1+B1'` → evaluated number) or custom formatting.
*/
renderCell?: (value: CellValue | undefined, ref: CellRef) => ReactElement | string
}
SpreadsheetSelectionInclusive rectangular selection — r1/c1 is the anchor (where the
drag started) and r2/c2 is the focus (where it ended). Both are
0-indexed. A single-cell selection has r1 === r2 && c1 === c2.
interface SpreadsheetSelection {
r1: number
c1: number
r2: number
c2: number
}
CellMapSparse map from CellRef to value. Cells not present in the map render
as empty. Using a Map keeps update cost O(1) regardless of grid size.
type CellMap = Map<CellRef, CellValue>
CellRefCell reference in A1 notation (e.g. 'A1', 'AB17'). Column letters are
uppercase; row numbers are 1-indexed.
type CellRef = string
CellValuePrimitive value stored in a cell. Formula strings start with '=' —
evaluation is the host application's responsibility (typically wired
through @molecule/api-formula-engine).
type CellValue = string | number | boolean | null
cellRef(row, col)Build an A1 reference from 0-indexed row and column numbers.
function cellRef(row: number, col: number): string
row — 0-indexed row number.col — 0-indexed column number.Returns: A1-style cell reference (e.g. 'B3').
columnLetter(col)Convert a 0-indexed column number to its A1 letter (0 → 'A', 25 →
'Z', 26 → 'AA', 701 → 'ZZ', 702 → 'AAA').
function columnLetter(col: number): string
col — 0-indexed column number.Returns: Uppercase A1 column letter.
computeVisibleRange(scroll, viewport, itemSize, total, overscan)Compute the inclusive [start, end] window of indices to render given
the scroll offset, viewport size, item size, and total count. Adds an
overscan margin on each side so newly-visible cells are pre-rendered
during fast scrolls.
function computeVisibleRange(
scroll: number,
viewport: number,
itemSize: number,
total: number,
overscan?: number,
): [number, number]
scroll — Current scroll position (px).viewport — Viewport size (px) along the scroll axis.itemSize — Per-item size (px) along the scroll axis.total — Total number of items.overscan — Extra items to render on each side (default 2).Returns: [startIndex, endIndex] (both inclusive, clamped to [0, total)).
formatCellValue(value)Format a cell value for display. null/undefined render as the empty
string; booleans become TRUE/FALSE (matching common spreadsheet
tools); numbers and strings stringify as-is.
function formatCellValue(value: CellValue | undefined): string
value — The raw cell value.Returns: Display string.
isInSelection(sel, row, col)Test whether (row, col) is inside the selection.
function isInSelection(sel: SpreadsheetSelection, row: number, col: number): boolean
sel — Selection to test against.row — 0-indexed row number.col — 0-indexed column number.Returns: true when the cell is in the selection.
normalizeSelection(sel)Normalize a possibly-inverted selection so r1 <= r2 && c1 <= c2.
function normalizeSelection(sel: SpreadsheetSelection): SpreadsheetSelection
sel — Raw selection (anchor and focus).Returns: Selection with r1/c1 as top-left and r2/c2 as bottom-right.
parseClipboardTsv(text)Parse a TSV (tab-separated values) string into a 2D array of strings. Empty trailing lines are ignored. Used to ingest clipboard paste data.
function parseClipboardTsv(text: string): string[][]
text — Clipboard text (typically TSV).Returns: 2D array of cell strings — rows[r][c].
serializeSelectionTsv(cells, sel)Serialize a rectangular selection to TSV (tab-separated values) so it can round-trip with Excel/Google Sheets via the system clipboard.
function serializeSelectionTsv(cells: CellMap, sel: SpreadsheetSelection): string
cells — Source cell map.sel — Selection to serialize.Returns: TSV-formatted string with \n row separators.
SpreadsheetGrid(props)High-performance virtualized spreadsheet cell grid.
Renders an rows × columns grid backed by a sparse Map<cellRef, value>,
with frozen rows/columns, range selection (click + drag, Shift+click),
copy/paste through the system clipboard as TSV, and in-cell editing
(double-click → input; Enter commits, Escape cancels).
Only the cells inside the visible viewport (plus a small overscan margin) are rendered, so 10k × 10k grids stay responsive.
Pairs with @molecule/api-formula-engine for formula evaluation —
pass an evaluated cells map and a renderCell that can look up
formula results.
function SpreadsheetGrid(
props: SpreadsheetGridProps,
): ReactElement<unknown, string | JSXElementConstructor<any>>
props — Component props (see {@link SpreadsheetGridProps}).Returns: The rendered grid.
Peer dependencies:
@molecule/app-i18n ^1.0.1@molecule/app-react ^1.0.1@molecule/app-ui ^1.0.1react ^18.0.0 || ^19.0.0@molecule/app-i18n
@molecule/app-react
@molecule/app-ui
react
Must render inside the app's i18n provider and with a ClassMap bond
wired (useTranslation() / getClassMap() throw otherwise).
The viewport is FIXED-PIXEL: viewportWidth/viewportHeight
(defaults 720×360) — the grid does not auto-size to its container.
Measure the container and pass px if you need it to fill.
Copy (Ctrl/Cmd+C) writes TSV via navigator.clipboard — secure
contexts only (HTTPS/localhost). Paste is handled through the native
paste event, so the grid container must have focus (click a cell
first). Keyboard support is Enter/F2 (edit) + copy/paste only; there
is NO arrow-key cell navigation.
Committed edits and pasted cells auto-coerce number-like strings to
numbers ('42' → 42); empty string clears the cell (null).
Gridlines/selection/header styling uses Material-3 design-token
utilities (bg-surface, border-outline-variant, …). Apps whose
Tailwind theme does not define those tokens (the minimal scaffold
theme does not) get a functional but unstyled grid — flagship-derived
themes render it fully.
Translation strings are provided by @molecule/app-locales-spreadsheet-grid.