Skip to content

InlineEditField

<InlineEditField> is a single-property cell. Default state shows plain text; click (or focus + Enter) turns it into an editor. Blur or Enter commits; Esc cancels. The value flips immediately, with a brief ✓ on success or × + message on failure (with rollback).

import from "@na/ui/components/InlineEditField" ▶ Open in Storybook packages/ui/src/components/InlineEditField.tsx

When to use

  • A property cell on an entity detail page (CRM person/deal) where edits are individual.
  • Quick rename / quick description / quick tag-edit affordances.

Don’t use InlineEditField for: forms (Form — has Save / Cancel and field-level validation), bulk editing, or any change that requires confirmation before persisting (use a modal).

Basic

Click the value, type, blur or Enter to commit. Esc cancels.

Name
const [name, setName] = useState('Acme Corp');
<InlineEditField
value={name}
onCommit={async (next) => {
await api.update({ name: next });
setName(next);
}}
placeholder="Add a name"
aria-label="Name"
/>

Server validation rejects

Reject in onCommit to roll back the optimistic value and surface an inline error.

Name (empty rejects)
<InlineEditField
value={name}
onCommit={async (next) => {
if (!next.trim()) throw new Error('Name cannot be empty.');
await api.update({ name: next });
setName(next);
}}
aria-label="Name"
/>

Read-only

Name (read-only)
<InlineEditField value="Acme Corp" onCommit={() => undefined} readOnly aria-label="Name" />

API

Prop Type Default Description
value * string | number | null | undefined Current value. Rendered as plain text in read mode.
onCommit * (next: string) => void | Promise<unknown> Resolve = success (✓ flash). Reject = failure (× + message, rollback).
type "text" | "url" | "number" | "date" | "select" | "person" "text" Edit-mode input flavour. select requires `options`.
options Array<{ value: string; label: string }> Required when type="select".
placeholder string Shown in read mode when the value is empty.
readOnly boolean false Renders as plain text only — no edit affordance.
formatDisplay (value) => ReactNode Override the read-mode text (e.g., formatted dates).
successTimeout number 1500 Milliseconds the ✓ stays visible after a successful commit.
inputProps InputProps Forwarded to the underlying Input in edit mode.
aria-label string Required when there is no surrounding label.

Design guidelines

✓ Do

  • Pair every InlineEditField with an aria-label or a surrounding label that describes the property.
  • Reject in onCommit to surface validation. The component handles rollback + inline error for you.
  • Use formatDisplay for dates and numbers — show the user-readable form in read mode.

✗ Don't

  • Wrap InlineEditField in a Form. It bypasses the react-hook-form pattern by design.
  • Add a Save button next to it. The blur / Enter contract is the contract.
  • Use it for fields that require multi-step confirmation (destructive renames, role changes). Use a modal.

Accessibility

  • Read mode is a focusable element; Enter / Space activates edit mode.
  • Edit mode is a real <Input>Esc cancels, Enter commits, blur commits.
  • Errors surface inline with role="alert" so screen readers announce them on rejection.

▶ Open InlineEditField stories in Storybook