Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Editable Cell

Inline-editable value with an async commit lifecycle — optimistic display, saving/saved/error status, and revert on a rejected write.

Status
stable
Since
0.4.0
Accessibility pattern
button-to-editor disclosure

Last updated

Account nameAcme Corporation

Click to edit. The commit is async — watch the saving indicator.

Install

Add Editable Cell from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/editable-cell

The same command installs the registry items it composes: @vegastack/auto-save-input, @vegastack/field-inline, @vegastack/select, @vegastack/spinner, @vegastack/use-announcer, @vegastack/use-inline-edit.

Usage

import { EditableCell } from "@/components/ui/editable-cell";

<EditableCell
  value={deal.name}
  label="Deal name"
  onCommit={(name) => api.updateDeal({ name })}
/>;

EditableCell composes FieldInline as its text leaf and layers on what every optimistic inline edit needs beyond the edit interaction itself: an idle → saving → saved | error status indicator (the same AutoSaveStatus vocabulary AutoSaveInput uses), conflict revert, and a typed per-type editor.

When onCommit returns a promise, the cell shows the committed value optimistically with a saving indicator. On resolve it flips to saved; on reject it reverts the display to value and politely announces the revert — the version_conflict path every optimistic CRM edit needs.

Scope

What this component deliberately does not own, in the style of DataList's scope table:

BehaviourWhere it lives
Persistence, retries, conflict checkHost — onCommit fires once per commit; a rejected promise = revert
Debounced auto-save while typingAutoSaveInput
The grid's roving focus modelThe grid host — focusMode="managed" only removes this tab stop
Date / actor / currency editorsApps, via the open custom editor contract

Examples

Conflict revert

Deal amount (server always rejects)$12,400

Every commit is rejected: the value snaps back and the revert is announced.

Select editor

Real inline editors are per-type: a stage field edits with a popover list, not a text box. The select editor renders a Select whose popover is the editor; commit fires on selection.

Stage

States

Read-onlyACME-2041
DisabledNorthwind Traders
Saving (controlled status)Renewal 2027

Managed focus (grid hosts)

Inside a grid, one tab stop per editable cell per row is an ergonomics regression — the grid's roving focus model owns reachability. focusMode="managed" removes the cell's own tab stop, and the host opens the editor from its keyboard model through the controlled editing / onEditingChange pair:

<EditableCell
  value={cell.value}
  label={column.header}
  focusMode="managed"
  editing={isEditingThisCell}
  onEditingChange={(editing) => grid.setEditingCell(editing ? cellId : null)}
  status={grid.cellStatus(rowId, column.key)}
  onCommit={(next) => grid.commitCell(rowId, column.key, next)}
/>

API Reference

PropTypeDefaultDescription
onCommit*(next: string) => void | Promise<void>Commit callback. Return a promise to engage the async status layer: the cell shows the committed value optimistically with a saving indicator, flips to saved on resolve, and on reject **reverts to value** and announces the revert (the version_conflict path). Return void for synchronous hosts.
value*stringThe persisted value. The cell displays it, edits a draft of it, and reverts to it on a failed commit.
classNamestringExtra classes merged onto the cell root.
disabledbooleanfalseBlocks editing; the display is dimmed and out of the tab order.
editingbooleanControlled edit mode, forwarded to the underlying editor. Required in practice for managed hosts (the grid opens the editor on Enter/F2); omit for the built-in click / <kbd>Enter</kbd> / <kbd>Space</kbd> activation.
editorEditableCellEditor{ type: "text" }The editor to open. See EditableCellEditor.
focusMode"managed" | "standalone""standalone"Focus policy. standalone (a card, a property list) gives the cell its own tab stop. managed removes it — the host's roving focus model owns reachability and opens the editor through editing/onEditingChange.
labelstringAccessible name for the value being edited (e.g. "Deal amount"). Falls back the same way FieldInline does; the editor is never unnamed.
onEditingChange((editing: boolean) => void)Called when the cell wants to enter (true) or leave (false) edit mode.
readOnlybooleanfalseRenders the value as plain non-interactive text with no edit affordance.
refReact.Ref<HTMLSpanElement>Ref forwarded to the cell's root <span> (data-slot="editable-cell").
statusAutoSaveStatusControlled status override. Omit it to let the cell derive status from the onCommit promise; pass it when the host owns the write lifecycle (a grid's cellStatus). Uses AutoSaveStatus — the system's one vocabulary for async field writes.

Data attributes and CSS variables on EditableCell

AttributeValues
data-focus-modemirrors a prop or state value
data-slot"editable-cell" | "editable-cell-status"
data-statusmirrors a prop or state value

Editor contract

The custom editor receives EditableCellEditorProps:

PropTypeDefaultDescription
cancel*() => voidLeave edit mode without committing.
commit*(next: string) => voidCommit next and leave edit mode. No-ops the async layer when unchanged.
value*stringThe value being edited (the optimistic value while a commit is in flight).

Accessibility

  • The display element is a role="button" (via FieldInline) — Enter or Space opens the editor, mirroring a mouse click; the editor is never unnamed (labelplaceholder → generic fallback).
  • Status changes are announced through a visually hidden polite live region — "Saving…", "Saved", "Save failed — value reverted". The indicator icons differ by shape, never by colour alone.
  • Escape cancels, restores the prior value, and returns to the display element; a rejected commit reverts the value and announces it.
  • In focusMode="managed" the cell has no tab stop of its own — the host grid must own reachability and open the editor from its keyboard model.
KeyAction
Enter / SpaceOpen the editor from the focused display element.
EnterCommit the draft (text editor).
EscapeCancel and restore the prior value.
ContractStates tested
Behaviourdisplay, editing, saving, saved, error, disabled, read-only, managed-focus
Accessibilitykeyboard, labeled, status-announcement, semantic-html
Visualdefault, hover, saving, saved, error, disabled

Do / Don't

Do
Return a promise from onCommit so the cell can run the saving/saved/error lifecycle and revert on rejection.
Don't
Toast the failure and leave the stale optimistic value on screen — the inline status and the revert are the feedback.

On this page