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

Field Inline

Click-to-edit text — displays a value, swaps to a focused input on click, commits on Enter or blur, cancels on Escape.

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

Last updated

Task titleUntitled task

Click the value to rename. Enter to save, Esc to cancel.

Install

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

pnpm dlx shadcn@latest add @vegastack/field-inline

The same command installs the registry items it composes: @vegastack/input, @vegastack/use-inline-edit.

Usage

import { FieldInline } from "@/components/ui/field-inline";

<FieldInline value={title} onCommit={(next) => updateTitle(next)} />;

FieldInline shows value as plain text with a hover affordance. Clicking it (or pressing Enter / Space when focused) swaps in a focused Input. It is purely presentational: onCommit fires only when the value actually changed, and your app owns persistence — the handler can be synchronous or kick off an async mutation.

The example above is interactive: click the value to enter edit mode, then press

Enter to commit or Escape to cancel.

Examples

Empty state

When value is empty, display mode renders the placeholder as muted text so there is always something to click. If no placeholder is supplied either, it falls back to a generic "Edit value" label — which also becomes the display element's accessible name, so the control is never unnamed even when blank.

Empty + placeholderAdd a title…
Empty, no placeholderEdit value

Accessible name

label names the edit-mode input for assistive tech without rendering any visible text. placeholder, by contrast, is the visible empty-state hint and is used as the accessible name only as a last resort. Prefer label (or aria-label / aria-labelledby) for a clear semantic name.

Named by label (invisible)Acme Corp
Named by placeholder (visible)Company name

Borderless

Set borderless to make the resting border transparent and remove the background and padding so the edit state sits seamlessly within surrounding text. The retained border geometry still provides the text-entry focus tint.

Document nameQ3 planning doc

Controlled edit mode

editing / onEditingChange lift the edit state — for hosts (an EditableCell in a grid) whose own keyboard model decides when the editor opens. tabIndex={-1} removes the display element from the tab order when a roving focus model owns reachability. After a keyboard commit or cancel, focus returns to the display element (a blur-commit never steals focus back).

Playground

Toggle borderless, disabled, and read-only on a click-to-edit value, then copy the generated JSX.

Quarterly planning notes
<FieldInline value={title} onCommit={setTitle} label="Document title" />

API Reference

PropTypeDefaultDescription
onCommit*(value: string) => voidCalled with the new (trimmed) value when the user commits via <kbd>Enter</kbd> or blur. Fired only when the value actually changed; the component is presentational, so the app owns persistence (sync or async).
value*stringThe current value, shown as text in display mode and seeded into the input on edit.
aria-labelstringOptional aria-label passed straight to the edit-mode input (wins over label).
aria-labelledbystringOptional aria-labelledby passed straight to the edit-mode input (wins over aria-label).
borderlessbooleanfalseStrip the input's border, background, and padding in edit mode for a seamless inline-text feel (e.g. editing a title in place).
classNamestringExtra classes merged onto the outer wrapper.
disabledbooleanfalseBlocks entering edit mode — clicking or pressing <kbd>Enter</kbd>/<kbd>Space</kbd> on the display value no longer starts an edit, and the display root is dimmed and dropped from the tab order (aria-disabled + tabIndex={-1}, since it's a role="button" span rather than a native control). If the field is mid-edit when disabled turns on, the edit is cancelled (reverted, onCommit is not called) the same way <kbd>Escape</kbd> does.
editingbooleanControlled edit mode. Pair with onEditingChange to own when the field edits — e.g. a cell host whose grid keyboard model opens the editor with <kbd>Enter</kbd>/<kbd>F2</kbd>. Omit for the built-in uncontrolled behaviour (click / <kbd>Enter</kbd> / <kbd>Space</kbd> on the display).
errorstringValidation error message. When set: the edit-mode Input receives aria-invalid (which drives its built-in destructive-border styling) plus aria-describedby pointing at the error text, and the error text itself renders below the control — same treatment as field.tsx's FieldError (role="status", text-sm text-destructive-text). Shown in both display and edit mode whenever it's set.
labelstringAccessible name for the edit-mode <input> (its aria-label). Prefer a clear semantic name (e.g. "Task title") so screen-reader users hear what they're editing even when no placeholder is set. Resolution order for the input's accessible name: aria-labelledbyaria-labellabelplaceholder → a generic fallback. The edit-mode textbox is therefore never unnamed.
onEditingChange((editing: boolean) => void)Called when the field wants to enter (true) or leave (false) edit mode — on activation, commit, and cancel. With editing controlled, the parent decides whether the mode actually changes.
placeholderstringPlaceholder shown in the input, and — when value is empty — as muted text in display mode. Used as the input's accessible name only as a fallback when neither label nor aria-label/aria-labelledby is supplied.
readOnlybooleanfalseRenders the value as plain, non-interactive text — no role="button", no hover affordance, no click/keyboard handler, and edit mode can never be entered. Unlike disabled, the text is not dimmed (it reads as normal content, just not editable here). If the field is mid-edit when readOnly turns on, the edit is cancelled the same way disabled does.
refReact.Ref<HTMLElement>Ref forwarded to the component's root host element — the display <span> when idle, the edit <input> while editing (the root swaps with mode).
tabIndexnumber0Tab-stop override for the display element. Pass -1 to remove it from the tab order when a host (a grid's roving focus model) owns reachability. Ignored when readOnly (no tab stop at all); disabled always renders -1.

Data attributes and CSS variables on FieldInline

AttributeValues
data-slot"field-inline" | "field-inline-error"

Accessibility

  • The display element is a role="button" in the tab order — Enter or Space enters edit mode, mirroring a mouse click.
  • Entering edit mode focuses the input and selects its contents, so typing replaces the value immediately.
  • Enter or blur commits; Escape cancels and restores the original value without firing onCommit.
  • The display affordance and edit-mode input are never unnamed. Accessible-name resolution is aria-labelledbyaria-labellabelplaceholder"Edit value". When value and placeholder are both empty, the display mode visibly renders "Edit value" as the fallback.
  • Focus treatment differs by mode: the display affordance is a role="button" <span> on the shared interactive-surface recipe (so its hover wash is visible on every surface, not just the page ground) and the default browser focus outline (no custom ring class). The edit-mode Input hides its outline and re-colors its border on focus via focus:border-ring/(--alpha-tint-border) — a border recolor, not a 2px ring. The error message announces as a polite role="status", matching Field.
ContractStates tested
Behaviourdefault, disabled, empty, error, invalid, open, read-only
Accessibilitydescribed, disabled, invalid, keyboard, labeled, alert-announcement, semantic-html
Visualdefault, hover, disabled, invalid, error, empty, dark

Do / Don't

Do
Prefer a meaningful label or aria-labelledby, and use placeholder for the visible empty state.
Don't
Treat onCommit like an onChange — it fires once per commit (only when changed), not on every keystroke.

On this page