@widgetkit/spreadsheet-react

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.

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.

Display
Features
Actions
=
A
B
C
D
E
F
G
H
1
2
Name
Q1
Q2
Q3
Total
3
Alice
$9,200.00
$10,500.00
$11,800.00
$31,500.00
4
Bob
$7,400.00
$8,100.00
$9,300.00
$24,800.00
5
Carol
$11,000.00
$12,500.00
$13,200.00
$36,700.00
6
Total
$27,600.00
$31,100.00
$34,300.00
$93,000.00
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Q1–Q3 Sales Report

Getting started

Install the package from npm:

npm install @widgetkit/spreadsheet-react
# or
pnpm add @widgetkit/spreadsheet-react

Import 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}
    />
  );
}

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}
    />
  );
}

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, ISTEXT

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}
/>

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 page

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.

=
A
B
C
D
E
F
G
H
I
J
1
2
Product
Category
Price
Units Sold
Revenue
Cost
Profit
Margin %
3
Pro Keyboard
Hardware
$129.00
412
$53,148.00
$29,231.40
$23,916.60
45.0%
4
Wireless Mouse
Hardware
$49.00
873
$42,777.00
$23,527.35
$19,249.65
45.0%
5
USB-C Hub
Hardware
$79.00
654
$51,666.00
$28,416.30
$23,249.70
45.0%
6
Webcam 4K
Hardware
$199.00
218
$43,382.00
$23,860.10
$19,521.90
45.0%
7
Desk Lamp
Furniture
$59.00
1102
$65,018.00
$35,759.90
$29,258.10
45.0%
8
Monitor Stand
Furniture
$89.00
540
$48,060.00
$26,433.00
$21,627.00
45.0%
9
Cable Tray
Furniture
$35.00
789
$27,615.00
$15,188.25
$12,426.75
45.0%
10
Laptop Stand
Furniture
$45.00
934
$42,030.00
$23,116.50
$18,913.50
45.0%
11
Note-taking App
Software
$9.00
3204
$28,836.00
$8,650.80
$20,185.20
70.0%
12
Task Manager
Software
$12.00
2871
$34,452.00
$10,335.60
$24,116.40
70.0%
13
PDF Editor
Software
$29.00
1540
$44,660.00
$13,398.00
$31,262.00
70.0%
14
VPN Client
Software
$8.00
4210
$33,680.00
$10,104.00
$23,576.00
70.0%
15
Screen Recorder
Software
$19.00
2108
$40,052.00
$12,015.60
$28,036.40
70.0%
16
Cloud Storage
Software
$6.00
6300
$37,800.00
$11,340.00
$26,460.00
70.0%
17
Totals
25755
$593,176.00
$271,376.80
$321,799.20
54.3%
18
19
20
21
22
23
Product Catalog — 2024
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}
    />
  );
}

Props

PropTypeDefaultDescription
Data
cellsCellMapControlled cell values. Use with onCellsChange.
defaultCellsCellMapInitial cell values for uncontrolled usage.
formatsRecord<string, CellFormat>Controlled cell formats (bold, color, number format, etc.).
defaultFormatsRecord<string, CellFormat>Initial formats for uncontrolled usage.
mergesRecord<string, MergeRegion>Controlled merge regions.
defaultMergesRecord<string, MergeRegion>Initial merges for uncontrolled usage.
Grid
rowsnumber100Number of rows in the grid.
colsnumber26Number of columns in the grid.
rowHeightnumber28Default row height in pixels.
defaultColWidthnumber100Default column width in pixels.
maxHeightnumberMaximum container height in pixels. Enables vertical scrolling.
Visibility
showFormulaBarbooleantrueShow the formula bar above the grid.
showToolbarbooleantrueShow the formatting toolbar.
showRowNumbersbooleantrueShow row number column on the left.
showColHeadersbooleantrueShow column letter headers at the top.
showContextMenubooleantrueShow the right-click context menu.
Interaction
readOnlybooleanfalseDisable all editing. Selection and copying still work.
resizableColsbooleantrueAllow columns to be resized by dragging their borders.
resizableRowsbooleantrueAllow rows to be resized by dragging their borders.
selectionMode"single" | "range""range"Whether multi-cell range selection is allowed.
autoExpandRowsbooleantrueAutomatically add rows when the last row is reached.
autoExpandColsbooleanfalseAutomatically add columns when the last column is reached.
Callbacks
onCellsChange(cells: CellMap) => voidCalled with the full updated cell map after any edit.
onCellChange(ref: string, value: CellValue) => voidCalled for each individual cell edit.
onFormatsChange(formats: Record<string, CellFormat>) => voidCalled when cell formatting changes.
onMergesChange(merges: Record<string, MergeRegion>) => voidCalled when cell merges change.
onSelectionChange(ref: string) => voidCalled when the selected cell or range changes.
onCellClick(ref: string, value: CellValue, e: MouseEvent) => voidCalled on single click.
onCellDoubleClick(ref: string, value: CellValue, e: MouseEvent) => voidCalled on double-click.
Advanced
renderCell(ref, value, format?) => ReactNodeCustom cell renderer. Return null to use the default.
onBeforeEdit(ref: string, current: CellValue) => booleanReturn false to prevent editing a specific cell.
onValidate(ref: string, value: CellValue) => string | nullReturn an error string to reject the new value, or null to accept.
contextMenuItemsArray<ContextMenuItem | null>Extra items appended to the right-click context menu. null inserts a divider.
aria-labelstringAccessible label for the grid element.

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;
}