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
- Since
0.1.0- Accessibility pattern
- button-to-editor disclosure
Last updated
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-inlineThe 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.
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.
label (invisible)Acme Corpplaceholder (visible)Company nameBorderless
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.
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.
<FieldInline value={title} onCommit={setTitle} label="Document title" />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
onCommit* | (value: string) => void | — | Called 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* | string | — | The current value, shown as text in display mode and seeded into the input on edit. |
aria-label | string | — | Optional aria-label passed straight to the edit-mode input (wins over label). |
aria-labelledby | string | — | Optional aria-labelledby passed straight to the edit-mode input (wins over aria-label). |
borderless | boolean | false | Strip the input's border, background, and padding in edit mode for a seamless inline-text feel (e.g. editing a title in place). |
className | string | — | Extra classes merged onto the outer wrapper. |
disabled | boolean | false | Blocks 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. |
editing | boolean | — | Controlled 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). |
error | string | — | Validation 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. |
label | string | — | Accessible 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-labelledby → aria-label → label →
placeholder → 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. |
placeholder | string | — | Placeholder 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. |
readOnly | boolean | false | Renders 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. |
ref | React.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). |
tabIndex | number | 0 | Tab-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
| Attribute | Values |
|---|---|
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-labelledby→aria-label→label→placeholder→"Edit value". Whenvalueandplaceholderare 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-modeInputhides its outline and re-colors its border on focus viafocus:border-ring/(--alpha-tint-border)— a border recolor, not a 2px ring. The error message announces as a politerole="status", matchingField.
| Contract | States tested |
|---|---|
| Behaviour | default, disabled, empty, error, invalid, open, read-only |
| Accessibility | described, disabled, invalid, keyboard, labeled, alert-announcement, semantic-html |
| Visual | default, hover, disabled, invalid, error, empty, dark |