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.
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>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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
| Prop | Type | Default | Description |
|---|---|---|---|
rows | number | 20 | Number of data rows (excluding the column header) |
cols | number | 8 | Number of columns, named A, B, C…Z, AA |
value | Record<string, string> | — | Controlled cell contents; keys are A1 addresses, values are raw input, a leading = makes a formula |
defaultValue | Record<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 |
columnWidths | Record<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 |
frozenRows | number | 1 | Besides the column header, the first N data rows stay on top while scrolling |
frozenCols | number | 0 | Besides the row numbers, the first N data columns stay on the left while scrolling |
readOnly | boolean | false | Read-only: cells can be selected and copied but not edited, cleared or pasted into |
formatNumber | (value: number) => string | zh-TW thousands separators (en-US in the English build) | Number formatter; defaults to up to 10 decimals |
maxHeight | number | 360 | Maximum height of the scrollable grid area (px) |
label | string | "試算表" (Spreadsheet; "Spreadsheet" in the English build) | Accessible name of the grid |
className | string | — | Merged into the root container |
SpreadsheetSelection
| Field | Type | Default | Description |
|---|---|---|---|
active | string | — | Active cell address, e.g. B3 |
range | string | — | Selected range, e.g. B3:D5; equals active for a single cell |
raw | string | — | Raw input of the active cell |
display | string | — | 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
| Field | Type | Default | Description |
|---|---|---|---|
--wb-spreadsheet-grid-stiffness | number | 600 | Spring stiffness of the selection box (damping is fixed at 40) |
--wb-spreadsheet-grid-flash | time | 400ms | Fade-out time of the accent flash after a commit |
--wb-spreadsheet-grid-accent | color | #2563eb | Accent color of the selection box and flash |
How it works
- Formulas: start with
=. Numbers, cell references ($absolute markers are accepted and ignored), ranges likeA1:B3,+ - * /, parentheses and unary minus are supported, plusSUM,AVERAGE,MIN,MAXandCOUNT(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/colschange. 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
clipboardDataon 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/frozenColspin the first N data rows / columns as well - Performance: every cell is rendered directly, so keep
rows × colswithin 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 newcolumnWidthsobject 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"witharia-rowcount/aria-colcount/aria-multiselectable; each row isrole="row", column headers arerole="columnheader", row numbers arerole="rowheader", and cells arerole="gridcell"witharia-rowindex,aria-colindexandaria-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 whosearia-labelreads "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
Spectrum Monitor
A live scrolling spectrum and heat band: a canvas waterfall of band intensities drawn frame by frame, with pause, hover readouts and automatic pausing when off screen.
System Map Canvas
A pannable, zoomable system map: nodes and edges are generated from data, groups expand and collapse, and selecting a node highlights its connected paths and opens its details.