Skip to content
WANCRAFT

F.116Forms & Inputs

Inline Edit

Click-to-edit text that becomes a field without moving a pixel, with async save, revert-on-error and validation.

NewReactCSSTailwindintermediate

Overview

Inline Edit turns a title or paragraph into a field in place. The text is a real button ("Edit title: …"); activating it swaps in an input or textarea sized by an invisible copy of its own text with identical type metrics, so the line box never jumps. Enter or blur saves and Escape cancels; multiline fields use Ctrl/⌘+Enter. An async onSave shows the new text as pending with a spinner, and a rejection reverts to the previous text with the error message and a small shake. A validate function keeps the field open with an inline message. Focus returns to the text after a keyboard save or cancel, and a pencil affordance can be shown permanently for touch and keyboard discoverability.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/inline-edit.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/inline-edit.json

Method 02 · Copy and paste

  1. 1. No extra dependencies — React and Tailwind CSS only.
  2. 2. Copy the source from the Code tab into components/ui/inline-edit.tsx.
  3. 3. Import it and adjust props.
Dependencies
None
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { InlineEdit } from "@/components/ui/inline-edit";

export function InlineEditExample() {
  return (
    <InlineEdit
      label="title"
      defaultValue="Q4 launch plan"
      validate={(next) => (next ? null : "A title is required.")}
      onSave={(next) => console.log("Saved", next)}
    />
  );
}

03Props

PropTypeDefaultDescription
value / defaultValuestring— / ""Controlled text, or the initial text when uncontrolled.
labelstring"text"Field name used in the button label "Edit {label}: {value}" and the field's aria-label.
onSave(next: string) => void | Promise<void>—Persist the change. A returned promise shows a pending state; rejection reverts and shows the error message.
onCancel() => void—Called when Escape cancels an edit.
validate(next: string) => string | null | undefined—Return a message to block saving.
multilinebooleanfalseUse an auto-growing textarea; Enter adds a line and Ctrl/⌘+Enter saves.
placeholderstring"Untitled"Shown muted when the value is empty.
size"sm" | "md" | "lg""md"Shared type scale for the text and the field.
showEditButtonbooleanfalseAlways show the pencil (otherwise on hover/focus, and always on touch).
saveOnBlurbooleantrueSave when focus leaves; false cancels instead.
selectOnEditbooleantrueSelect the text when editing starts.
maxLengthnumber—Native maxLength for the field.
accentstring"#FF7448"Field outline, caret and focus ring colour.
disabledbooleanfalseDisables editing.

04Features

  • 01Text and field share type metrics; an invisible sizer keeps the line box identical
  • 02Enter or blur saves, Escape cancels; multiline uses Ctrl/⌘+Enter
  • 03Async onSave with a pending spinner and optimistic text
  • 04Rejected saves revert with the error message and a shake
  • 05validate keeps the field open with an inline message
  • 06Pencil affordance on hover/focus, always on touch, or always on
motion · tweeninput · clickinput · keyboardinput · focus

05Accessibility

  • The text is a native <button> named "Edit {label}: {value}"; the field has an aria-label and instructions linked by aria-describedby.
  • Focus moves into the field on edit and back to the button after Enter or Escape; blur saves leave focus where the user moved it.
  • Validation and save errors are shown in a polite live region and linked to the field or button; invalid fields set aria-invalid.
  • While saving the button is aria-busy and aria-disabled, and "Saving…" is announced.
  • prefers-reduced-motion removes the shake and the spinner animation.

06Limitations

  • Single-line fields scroll horizontally rather than wrapping; use multiline for long text.