Data Storytelling

Spreadsheet Grid

An editable spreadsheet grid: arrow-key selection, double-click or Enter to edit, a formula bar showing raw input, simple formulas, and a frozen header row and column.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

A small spreadsheet you can type straight into: arrow keys move, Shift + arrows extend the selection, double-click or Enter edits, and the formula bar on top shows the active cell's raw content (a formula shows as =B2*C2, not its result). A hand-written formula engine supports arithmetic, cell references, ranges and SUM / AVERAGE / MIN / MAX / COUNT, and uses a dependency graph to recalculate only the cells that are affected. The selection box springs to its new position, and after a commit the edited cell flashes together with every formula cell that depends on it, so you can see exactly what a change ripples into. The first key of an IME such as Zhuyin or Cangjie starts editing directly and is never lost.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/spreadsheet-grid.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

20
8
0
600
Active edited usageDefault props
<SpreadsheetGrid/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/spreadsheet-grid.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/spreadsheet-grid.

Usage

import * as React from "react";
import { SpreadsheetGrid } from "@/components/ui/spreadsheet-grid";

export function Budget() {
  const [cells, setCells] = React.useState<Record<string, string>>({
    A1: "品項",
    B1: "數量",
    C1: "單價",
    D1: "小計",
    A2: "便當",
    B2: "32",
    C2: "110",
    D2: "=B2*C2",
    A3: "合計",
    D3: "=SUM(D2:D2)",
  });

  return (
    <SpreadsheetGrid
      rows={20}
      cols={6}
      value={cells}
      onChange={setCells}
      columnWidths={{ A: 140 }}
      frozenRows={1}
      frozenCols={1}
      onSelectionChange={(s) => console.log(s.range, s.display)}
    />
  );
}

Props

PropTypeDefaultDescription
rowsnumber20Number of data rows (excluding the column header)
colsnumber8Number of columns, named A, B, C…Z, AA
valueRecord<string, string>—Controlled cell contents; keys are A1 addresses, values are raw input, a leading = makes a formula
defaultValueRecord<string, string>—Initial contents for uncontrolled mode
onChange(value: Record<string, string>) => void—Called whenever any cell changes, with the full new contents; cells cleared to an empty string are removed
columnWidthsRecord<string, number>—Column widths in px keyed by column letter; unspecified columns are 96. Drag a column header's right edge to resize (40–480)
onColumnResize(column: string, width: number) => void—Called on release after a column resize drag
onSelectionChange(selection: SpreadsheetSelection) => void—Called when the active cell or selected range changes
frozenRowsnumber1Besides the column header, the first N data rows stay on top while scrolling
frozenColsnumber0Besides the row numbers, the first N data columns stay on the left while scrolling
readOnlybooleanfalseRead-only: cells can be selected and copied but not edited, cleared or pasted into
formatNumber(value: number) => stringzh-TW thousands separators (en-US in the English build)Number formatter; defaults to up to 10 decimals
maxHeightnumber360Maximum height of the scrollable grid area (px)
labelstring"試算表" (Spreadsheet; "Spreadsheet" in the English build)Accessible name of the grid
classNamestring—Merged into the root container

SpreadsheetSelection

FieldTypeDefaultDescription
activestring—Active cell address, e.g. B3
rangestring—Selected range, e.g. B3:D5; equals active for a single cell
rawstring—Raw input of the active cell
displaystring—Displayed value of the active cell (the computed result or error code for formulas)

The SpreadsheetCellValue, SpreadsheetErrorCode and SpreadsheetGridProps types are exported by name as well.

CSS variables

FieldTypeDefaultDescription
--wb-spreadsheet-grid-stiffnessnumber600Spring stiffness of the selection box (damping is fixed at 40)
--wb-spreadsheet-grid-flashtime400msFade-out time of the accent flash after a commit
--wb-spreadsheet-grid-accentcolor#2563ebAccent color of the selection box and flash

How it works

  • Formulas: start with =. Numbers, cell references ($ absolute markers are accepted and ignored), ranges like A1:B3, + - * /, parentheses and unary minus are supported, plus SUM, AVERAGE, MIN, MAX and COUNT (case-insensitive, comma-separated arguments). Text and blanks inside ranges are skipped; in arithmetic a blank counts as 0 and text returns #VALUE!
  • Error codes: a reference outside the grid is #REF!, division by zero is #DIV/0!, an unknown function or name is #NAME?, a circular reference is #CYCLE!, and a syntax error is #ERROR!; errors propagate to dependent cells and render in rose
  • Only affected cells recalculate: the component keeps a dependency graph and, on change, recalculates only the edited cells and the formulas that transitively depend on them; the whole sheet is rebuilt only when rows / cols change. Parsed formulas are cached by their raw string
  • Keyboard: arrows move, Shift + arrows extend, Ctrl/⌘ + arrows jump to the edge, Home / End go to the start / end of the row; Tab / Shift+Tab move right / left; Enter starts editing, and while editing Enter commits and moves down (Shift+Enter moves up); F2 edits; typing overwrites; Esc cancels; Delete / Backspace clears the selection; Ctrl/⌘+A selects all
  • Copy and paste: Ctrl/⌘+C copies the selected range's raw content as TSV (formulas are copied as formula text, without shifting relative references); Ctrl/⌘+V pastes TSV from the top-left of the selection and trims anything beyond the grid. It uses clipboardData on the copy / paste events, so no clipboard permission is needed
  • Mouse: click to select, drag or Shift + click to extend, double-click to edit; click a column header to select the column, a row number to select the row, and the top-left corner to select all
  • Freezing: the column header and row numbers are always pinned; frozenRows / frozenCols pin the first N data rows / columns as well
  • Performance: every cell is rendered directly, so keep rows × cols within about 2,000 cells; paginate larger data outside the component — it does not virtualize
  • Syncing columnWidths: dragged widths are kept inside the component; passing a new columnWidths object resets them to the prop, so keep it in state or a constant rather than creating a new object every render

Accessibility

  • The grid is role="grid" with aria-rowcount / aria-colcount / aria-multiselectable; each row is role="row", column headers are role="columnheader", row numbers are role="rowheader", and cells are role="gridcell" with aria-rowindex, aria-colindex and aria-selected
  • Roving tabindex: only the active cell has tabIndex=0, so tabbing into the grid lands on it; focus is handed to a hidden input inside the active cell whose aria-label reads "Cell B3: value"
  • The formula bar input is labelled "Cell B3 content"; Shift+Tab in the first column returns to the formula bar, and Tab in the last column leaves the grid
  • Every Enter commit ignores keys pressed during IME composition (including Safari's keyCode 229), so picking a Zhuyin candidate never submits by accident; typing through an IME enters edit mode on compositionstart
  • The status bar (range, sum, average, count) is aria-live="polite"
  • When the user's system has Reduce Motion turned on, the selection box jumps straight to its new position without the spring, and the post-commit flash is not shown