Spreadsheet Cell
A single spreadsheet cell. Renders the computed value when idle and an inline <input> when editing. The keyboard model matches Excel / Google Sheets — double-click, Enter, or F2 to edit, type a printable key to overwrite, Escape to cancel, Enter or blur to commit. Pair with your own dependency-graph evaluator to drive the display prop for formulas and the state attribute for cells whose values just changed downstream of an edit.
A1
10
A2
20
A3
30
Customize
Behaviour
Installation
npx shadcn@latest add https://craftbits.dev/r/spreadsheet-cell.jsonUsage
import { SpreadsheetCell } from "@craft-bits/core";
<SpreadsheetCell
cellId="A1"
value="10"
onChange={(next, meta) => updateCell("A1", next, meta.formula)}
/>Formula cell with computed display — value is the raw formula text, display is the evaluation result:
<SpreadsheetCell
cellId="A3"
value="=A1+A2"
display={30}
onChange={(next) => updateCell("A3", next)}
/>Drive selection / dependency highlights from your own algorithm reducer — the cell is pure render:
<SpreadsheetCell
cellId={id}
value={model[id].raw}
display={evaluate(id, model)}
state={
id === cursor ? "highlight" : affected.has(id) ? "affected" : "default"
}
error={errors[id] ?? null}
onChange={(next, meta) => dispatch({ type: "edit", id, next, meta })}
/>Understanding the component
- Two render modes, one element. The root
role="gridcell"div swaps between a<span>(idle) and an<input>(editing).tabIndexflips between0and-1so Tab walks idle cells but never dumps focus on an input that has already taken the cursor. - Three commit paths, one helper. Enter, blur, and (when controlled externally) a re-render with
editing=falseall funnel through onecommit(next)call. The helper updates uncontrolled state if any and firesonChangewith acellId / raw / formulameta payload — consumers do not have to re-detect the leading=. - Type-to-overwrite. When the cell is idle and the user presses a single printable key, that key is stashed in a ref and editing starts; the effect that runs on
editing=truereads the ref and seeds the input. That is the Excel / Sheets convention — start typing and the prior value is replaced, no double-click required. - Controlled / uncontrolled symmetry.
valueplusonChangeis controlled;defaultValuealone is uncontrolled. The same applies toeditingplusonEditStartandonCancel— passeditingto drive edit mode from outside (e.g. a cursor in a larger grid), or omit it and let the cell self-manage. - State via data attributes.
state,error,formula, andboldall surface asdata-*attributes on the root. Tailwind variant selectors drive every visual treatment — no className toggle gymnastics, and consumers can target the same attributes with their own CSS without forking the component.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
cellId | string | required | Stable identifier (e.g. "A1"). Echoed in the accessible name and the onChange meta. |
value | string | — | Controlled raw text. Pair with onChange. |
defaultValue | string | "" | Uncontrolled initial raw text. |
display | ReactNode | — | What the cell renders when idle. For formulas, pass the evaluation result. Defaults to value when omitted. |
error | string | null | null | Evaluation error. When set, replaces display and adds data-error="true". |
state | "default" | "highlight" | "affected" | "dimmed" | "default" | Semantic visual state. |
bold | boolean | false | Render text bold. |
editing | boolean | — | Controlled edit mode. Omit to let the cell self-manage. |
onEditStart | () => void | — | Fired when the user enters edit mode. |
onChange | (next, meta) => void | — | Fired on commit. meta is cellId / raw / formula. |
onCancel | () => void | — | Fired on Escape. |
width | string | '100%' | CSS width. |
height | string | '28px' | CSS height. |
aria-label | string | auto | Accessible name. Defaults to a cellId + value summary. |
className | string | — | Merged onto the root <div>. |
Accessibility
- Root element is a
role="gridcell"div so the cell drops cleanly into arole="grid"wrapper. - The accessible name defaults to a
cellId+ display summary (or an error summary when erroring, orcellId: empty). Override witharia-labelif your grid already names cells globally. - Editing surfaces a real
<input>so screen readers, IME composition, autofill, and OS spellcheck-disable all work natively. The input has its ownaria-labelofEdit <cellId>. - Idle cells are focusable (
tabIndex=0); the editing input gets focus, andtabIndexon the wrapper flips to-1so Tab does not hop back out mid-edit. - Keyboard: double-click, Enter, or F2 enter edit mode. Single printable keys enter edit mode with overwrite semantics. Enter commits, Escape cancels, blur commits.
data-state,data-formula,data-error,data-boldare styling hooks — consumers can re-skin with CSS alone, no className override required.
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/sdp-spreadsheet/ui/CellComponent.tsx). Stripped the TD CSS-module chrome and the four boolean flag tangle (isAffected,isHighlighted,isEditing,bold) in favour of a singlestateenum + a controllededitingprop, generalized the value/formula split into avalue/display/errortriple so the consumer's evaluator drives presentation, added controlled+uncontrolled editing, and pinned every state to adata-*attribute so consumers can re-theme without forking.