Spreadsheet
A lightweight spreadsheet with inline editing, formula evaluation, cell formatting, merges, and full keyboard navigation. Pass your data as a simple key-value map and receive updates via callbacks — no spreadsheet engine setup required.
Overview
Introduction
Spreadsheet renders an editable grid with A1-style cell references, formula evaluation, a formatting toolbar, and a formula bar. Works in both controlled and uncontrolled modes. Use the imperative API via ref to programmatically scroll, select, or export data.
Setup
Getting started
Install the package from npm:
npm install @widgetkit/spreadsheet-react
# or
pnpm add @widgetkit/spreadsheet-reactImport the component and its stylesheet:
import { Spreadsheet } from "@widgetkit/spreadsheet-react";
import "@widgetkit/spreadsheet-react/styles.css";Pass defaultCells for uncontrolled usage, or cells + onCellsChange for controlled:
import { Spreadsheet } from "@widgetkit/spreadsheet-react";
import "@widgetkit/spreadsheet-react/styles.css";
export function App() {
return (
<Spreadsheet
defaultCells={{
A1: "Name", B1: "Score",
A2: "Alice", B2: 95,
A3: "Bob", B3: 87,
A4: "Total", B4: "=SUM(B2:B3)",
}}
rows={10}
cols={5}
/>
);
}Example
Controlled mode
Pass cells and onCellsChange to keep your own state in sync with the grid. This is the recommended pattern when you need to persist data or react to changes externally. Use defaultCells instead for uncontrolled usage where you just need an initial value.
import { useState } from "react";
import { Spreadsheet } from "@widgetkit/spreadsheet-react";
import "@widgetkit/spreadsheet-react/styles.css";
import type { CellMap } from "@widgetkit/spreadsheet-react";
export function App() {
const [cells, setCells] = useState<CellMap>({
A1: "Name", B1: "Score",
A2: "Alice", B2: 95,
A3: "Bob", B3: 87,
A4: "Total", B4: "=SUM(B2:B3)",
});
return (
<Spreadsheet
cells={cells}
onCellsChange={setCells}
rows={10}
cols={4}
/>
);
}Example
Formulas
Any cell value starting with = is evaluated as a formula. Formulas support cell references (A1), ranges (A1:C3), and a built-in function library across four categories.
// Cells starting with "=" are evaluated as formulas
const cells: CellMap = {
A1: 100, B1: 200, C1: 300,
A2: "=SUM(A1:C1)", // 600
A3: "=AVERAGE(A1:C1)", // 200
A4: '=IF(A2>500, "High", "Low")', // "High"
A5: '=CONCAT("Total: ", A2)', // "Total: 600"
A6: '=IFERROR(A1/0, "Div error")', // "Div error"
};
// Supported functions
// Math: SUM, AVERAGE, MIN, MAX, ABS, ROUND, FLOOR, CEILING, SQRT, POWER, MOD
// Logic: IF, AND, OR, NOT, IFERROR
// Text: CONCAT, LEN, UPPER, LOWER, TRIM, LEFT, RIGHT, MID
// Info: ISBLANK, ISNUMBER, ISTEXTExample
Formatting & merges
Cell formatting is stored separately from cell values in a Record<string, CellFormat>. The toolbar writes to this map via onFormatsChange. You can also merge cells into a single region using merges.
import type { CellFormat, MergeRegion } from "@widgetkit/spreadsheet-react";
const formats: Record<string, CellFormat> = {
A1: { bold: true, background: "#1e293b", color: "#ffffff" },
B2: { italic: true, color: "#3b82f6" },
C3: { numberFormat: "currency" }, // $1,234.56
D4: { numberFormat: "percent", decimalPlaces: 1 }, // 42.0%
};
// A1 spans 3 columns
const merges: Record<string, MergeRegion> = {
A1: { colSpan: 3, rowSpan: 1 },
};
<Spreadsheet
cells={cells}
formats={formats}
merges={merges}
onFormatsChange={setFormats}
onMergesChange={setMerges}
/>Example
Imperative API
Pass a ref to access the imperative handle. Useful for programmatic navigation, focus management, or exporting data from outside the component.
import { useRef } from "react";
import { Spreadsheet } from "@widgetkit/spreadsheet-react";
import type { SpreadsheetHandle } from "@widgetkit/spreadsheet-react";
export function App() {
const ref = useRef<SpreadsheetHandle>(null);
return (
<>
<button onClick={() => ref.current?.scrollToCell("Z50")}>Scroll to Z50</button>
<button onClick={() => ref.current?.setSelection("B2")}>Select B2</button>
<button onClick={() => ref.current?.startEdit("A1")}>Edit A1</button>
<button onClick={() => console.log(ref.current?.exportCsv())}>Export CSV</button>
<Spreadsheet ref={ref} defaultCells={{ A1: "Hello" }} />
</>
);
}Built-in keyboard shortcuts:
Arrow keys Navigate cells
Enter / Tab Confirm edit, move to next cell
Escape Cancel edit
F2 Start editing selected cell
Delete / Backspace Clear selected cell(s)
Ctrl+C / Ctrl+V Copy / paste
Ctrl+X Cut
Ctrl+Z / Ctrl+Y Undo / redo
Ctrl+B / Ctrl+I Bold / italic
Ctrl+Home / End Jump to first / last cell
Page Up / Down Scroll by pageExample
Large dataset
A 14-row product catalog demonstrating formulas across multiple dependent columns, mixed number formats, conditional colors, a merged title cell, and a totals row — all driven by a flat CellMap.
import { useState } from "react";
import { Spreadsheet } from "@widgetkit/spreadsheet-react";
import "@widgetkit/spreadsheet-react/styles.css";
import type { CellMap, CellFormat, MergeRegion } from "@widgetkit/spreadsheet-react";
const cells: CellMap = {
A1: "Product Catalog — 2024",
A2: "Product", B2: "Category", C2: "Price", D2: "Units Sold",
E2: "Revenue", F2: "Cost", G2: "Profit", H2: "Margin %",
A3: "Pro Keyboard", B3: "Hardware", C3: 129, D3: 412,
A4: "Wireless Mouse", B4: "Hardware", C4: 49, D4: 873,
A5: "Note-taking App", B5: "Software", C5: 9, D5: 3204,
A6: "Task Manager", B6: "Software", C6: 12, D6: 2871,
// ...more rows
// Revenue = Price × Units
E3: "=C3*D3", E4: "=C4*D4", E5: "=C5*D5", E6: "=C6*D6",
// Profit = Revenue − Cost
G3: "=E3-F3", G4: "=E4-F4", G5: "=E5-F5", G6: "=E6-F6",
// Margin % = Profit ÷ Revenue
H3: "=G3/E3", H4: "=G4/E4", H5: "=G5/E5", H6: "=G6/E6",
// Totals
A7: "Totals",
D7: "=SUM(D3:D6)", E7: "=SUM(E3:E6)",
F7: "=SUM(F3:F6)", G7: "=SUM(G3:G6)",
H7: "=G7/E7",
};
const formats: Record<string, CellFormat> = {
A1: { bold: true, background: "#1e293b", color: "#ffffff" },
A2: { bold: true, background: "#f1f5f9" },
// ... header formatting for B2–H2
C3: { numberFormat: "currency" },
E3: { numberFormat: "currency" },
G3: { numberFormat: "currency", color: "#16a34a" },
H3: { numberFormat: "percent", decimalPlaces: 1 },
// ... same for each row
G7: { bold: true, numberFormat: "currency", color: "#16a34a", background: "#f0fdf4" },
H7: { bold: true, numberFormat: "percent", decimalPlaces: 1, color: "#16a34a", background: "#f0fdf4" },
};
const merges: Record<string, MergeRegion> = {
A1: { colSpan: 8, rowSpan: 1 }, // title spans all columns
};
export function App() {
const [currentCells, setCurrentCells] = useState<CellMap>(cells);
const [currentFormats, setCurrentFormats] = useState(formats);
const [currentMerges, setCurrentMerges] = useState(merges);
return (
<Spreadsheet
cells={currentCells}
formats={currentFormats}
merges={currentMerges}
rows={30}
cols={10}
maxHeight={420}
autoExpandRows
resizableCols
onCellsChange={setCurrentCells}
onFormatsChange={setCurrentFormats}
onMergesChange={setCurrentMerges}
/>
);
}Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| Data | |||
cells | CellMap | — | Controlled cell values. Use with onCellsChange. |
defaultCells | CellMap | — | Initial cell values for uncontrolled usage. |
formats | Record<string, CellFormat> | — | Controlled cell formats (bold, color, number format, etc.). |
defaultFormats | Record<string, CellFormat> | — | Initial formats for uncontrolled usage. |
merges | Record<string, MergeRegion> | — | Controlled merge regions. |
defaultMerges | Record<string, MergeRegion> | — | Initial merges for uncontrolled usage. |
| Grid | |||
rows | number | 100 | Number of rows in the grid. |
cols | number | 26 | Number of columns in the grid. |
rowHeight | number | 28 | Default row height in pixels. |
defaultColWidth | number | 100 | Default column width in pixels. |
maxHeight | number | — | Maximum container height in pixels. Enables vertical scrolling. |
| Visibility | |||
showFormulaBar | boolean | true | Show the formula bar above the grid. |
showToolbar | boolean | true | Show the formatting toolbar. |
showRowNumbers | boolean | true | Show row number column on the left. |
showColHeaders | boolean | true | Show column letter headers at the top. |
showContextMenu | boolean | true | Show the right-click context menu. |
| Interaction | |||
readOnly | boolean | false | Disable all editing. Selection and copying still work. |
resizableCols | boolean | true | Allow columns to be resized by dragging their borders. |
resizableRows | boolean | true | Allow rows to be resized by dragging their borders. |
selectionMode | "single" | "range" | "range" | Whether multi-cell range selection is allowed. |
autoExpandRows | boolean | true | Automatically add rows when the last row is reached. |
autoExpandCols | boolean | false | Automatically add columns when the last column is reached. |
| Callbacks | |||
onCellsChange | (cells: CellMap) => void | — | Called with the full updated cell map after any edit. |
onCellChange | (ref: string, value: CellValue) => void | — | Called for each individual cell edit. |
onFormatsChange | (formats: Record<string, CellFormat>) => void | — | Called when cell formatting changes. |
onMergesChange | (merges: Record<string, MergeRegion>) => void | — | Called when cell merges change. |
onSelectionChange | (ref: string) => void | — | Called when the selected cell or range changes. |
onCellClick | (ref: string, value: CellValue, e: MouseEvent) => void | — | Called on single click. |
onCellDoubleClick | (ref: string, value: CellValue, e: MouseEvent) => void | — | Called on double-click. |
| Advanced | |||
renderCell | (ref, value, format?) => ReactNode | — | Custom cell renderer. Return null to use the default. |
onBeforeEdit | (ref: string, current: CellValue) => boolean | — | Return false to prevent editing a specific cell. |
onValidate | (ref: string, value: CellValue) => string | null | — | Return an error string to reject the new value, or null to accept. |
contextMenuItems | Array<ContextMenuItem | null> | — | Extra items appended to the right-click context menu. null inserts a divider. |
aria-label | string | — | Accessible label for the grid element. |
Reference
TypeScript types
All types are exported from @widgetkit/spreadsheet-react. Core types (CellValue, CellMap) are also available from @widgetkit/spreadsheet if you need them framework-agnostically.
import type {
CellValue,
CellMap,
CellFormat,
MergeRegion,
NumberFormat,
SpreadsheetHandle,
SpreadsheetProps,
} from "@widgetkit/spreadsheet-react";type CellValue = string | number | null;
type CellMap = Record<string, CellValue>;
type NumberFormat = 'general' | 'number' | 'currency' | 'percent' | 'date';
interface CellFormat {
bold?: boolean;
italic?: boolean;
color?: string; // CSS color
background?: string; // CSS color
numberFormat?: NumberFormat;
decimalPlaces?: number;
}
interface MergeRegion {
colSpan: number;
rowSpan: number;
}
// Imperative handle — access via ref
interface SpreadsheetHandle {
scrollToCell: (ref: string) => void;
setSelection: (ref: string) => void;
startEdit: (ref: string) => void;
exportCsv: () => string;
getCells: () => CellMap;
}