Inputs

Inline Edit Field

A click-to-edit inline field — the display and edit states swap seamlessly with a layout morph, Enter/blur submits and Esc cancels, with multiline support, controlled and uncontrolled modes, and no accidental submit mid-IME-composition.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add inline-edit-field.en

New project or installation trouble? Start here →

Inline Edit Field is a React inline edit (click-to-edit text) component built with Tailwind CSS and Motion, with lucide-react icons. It looks like plain text until you click it, then turns into an input in the same spot; single-line mode uses an input and multiline mode uses a textarea.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<InlineEditField/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • Titles of projects, documents or kanban cards that people rename right on the page.
  • Single fields in an admin list or settings page, such as team name, display name or notes.
  • A bio on a profile page, using multiline for longer text.
  • Not a good fit: forms where several fields are filled in and validated together; a regular form with a submit button is clearer there.

Installation

npx webberui@latest add https://webberui.com/r/inline-edit-field.en.json

Or, once registries are configured in components.json, install it as @webberui/inline-edit-field.

Usage

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

<InlineEditField
  value={title}
  onValueChange={setTitle}
  placeholder="Untitled event"
  aria-label="Event title"
/>

When value is omitted, the component manages the state internally (uncontrolled mode) and defaultValue supplies the initial value; passing value together with onValueChange puts it in controlled mode. onValueChange only fires on "submit" (Enter, blur, or pressing ✓), never on every keystroke.

Multiline mode:

<InlineEditField
  multiline
  maxRows={5}
  variant="outline"
  defaultValue="Add a short intro⋯"
/>

Examples

Save to an API on submit and roll back on failure

onValueChange only fires on submit, so you can call your API directly from it. In controlled mode, update the UI first and restore the old value if the request fails.

"use client";

import { useState } from "react";
import { InlineEditField } from "@/components/ui/inline-edit-field";

export function ProjectTitle({ id, initial }: { id: string; initial: string }) {
  const [title, setTitle] = useState(initial);

  async function save(next: string) {
    const prev = title;
    setTitle(next);
    const res = await fetch(`/api/projects/${id}`, {
      method: "PATCH",
      body: JSON.stringify({ title: next }),
    });
    if (!res.ok) setTitle(prev);
  }

  return (
    <InlineEditField
      value={title}
      onValueChange={save}
      placeholder="Untitled project"
      aria-label="Project name"
    />
  );
}

Reject empty values

The component does no validation itself. In controlled mode the display only changes when you update value, so you can filter invalid input inside onValueChange.

<InlineEditField
  value={name}
  onValueChange={(next) => {
    const trimmed = next.trim();
    if (trimmed) setName(trimmed);
  }}
  aria-label="Team name"
/>

Use it as a page heading

Put the font size on the outer className and both the display text and the input inherit it. This example hides the action buttons and treats blur as cancel, so only Enter saves.

<InlineEditField
  defaultValue="Q3 product roadmap"
  showActions={false}
  submitOnBlur={false}
  aria-label="Document title"
  className="text-2xl font-semibold"
/>

Props

PropTypeDefaultDescription
valuestring—Controlled value; when omitted, the component manages it internally
defaultValuestring""Initial value in uncontrolled mode
onValueChange(value: string) => void—Fires on submit (Enter / blur / pressing ✓) with the latest value
onEditingChange(editing: boolean) => void—Fires when the editing state toggles
placeholderstring"Click Edit..."Text shown when the value is empty, also used as the input hint
multilinebooleanfalseUse a multiline textarea (Enter inserts a newline, ⌘/Ctrl+Enter submits)
maxRowsnumber6Maximum rows in multiline mode; beyond that the input scrolls
disabledbooleanfalseDisabled: read-only display, editing cannot be entered
submitOnBlurbooleantrueWhether blurring submits; false treats blur as a cancel
selectOnEditbooleantrueWhether to select all the existing text when entering edit mode
showActionsbooleantrueWhether to show the ✓ / ✕ action buttons while editing
variant"ghost" | "outline""ghost"Display style: ghost (background only on hover/focus) or outline (a permanent border)
aria-labelstring—Accessible label; falls back to placeholder when omitted
classNamestring—Appended to the outermost container
inputClassNamestring—Appended to the input element (input / textarea)

How it works

  • Click to edit: the display state is a button occupying the same position and font size as the text; clicking it (or Tab-focusing it and pressing Enter / Space) swaps it for an input, and a pencil icon appears on the right on hover to signal that it is editable
  • Seamless morph: the display button and the edit area cross-fade through AnimatePresence mode="popLayout", while the outer container carries layout so a spring smoothly makes up the size difference and the text never jumps out of position; the edit state's border, background, and focus ring fade in with a CSS transition
  • Submit and cancel: single-line presses Enter to submit, Esc to cancel, and blur submits by default (submitOnBlur); multiline uses Enter for a newline, ⌘/Ctrl+Enter to submit, and Esc to cancel. onValueChange does not fire when the value is unchanged
  • Action buttons do not jump the gun: ✓ / ✕ block the default behaviour with onMouseDown to keep the input focused, avoiding a race where the click first triggers a blur submit
  • Multiline auto-grow: multiline grows with the content and scrolls inside the box past maxRows (using the textarea's own auto-resize)
  • IME safe: pressing Enter mid-composition in Chinese and other input methods does not accidentally submit (it checks both isComposing and keyCode !== 229, covering Safari's habit of firing keydown only after compositionend)

Accessibility

  • When the user has "reduce motion" enabled at the system level, all morph animations are disabled and display/edit switch instantly (the first render matches SSR; it is only disabled after mount)
  • The display state is a native <button> with an aria-label describing the value, takes Tab focus in order, and shows a ring on focus-visible
  • The input carries an aria-label (falling back to placeholder when omitted)
  • When editing ends via the keyboard or the action buttons, focus returns to the display button; when it ends because of a blur, focus is not stolen back, so it does not interfere with whatever the user actually clicked
  • ✓ / ✕ are native <button>s, each with a "confirm" and "cancel" aria-label

FAQ

Does it work with the Next.js App Router?

Yes. The file starts with "use client", so import it from a Server Component page as usual. If you save through a Server Action, call it from onValueChange.

When does onValueChange fire?

Only on submit: Enter in single-line mode, Cmd/Ctrl+Enter in multiline mode, the check button, or blur when submitOnBlur is on. It does not fire if the value is unchanged, and Esc or the cancel button discards the draft.

Will typing with an IME (Chinese, Japanese, Korean) submit by accident?

No. Pressing Enter while composing does not submit; the component checks both isComposing and keyCode 229, which covers Safari's different event order.

How do I change the look?

variant is either ghost (background only on hover or while editing) or outline (always-visible border). Add Tailwind classes to the outer container with className and to the input itself with inputClassName.

What are the dependencies?

motion, lucide-react, clsx and tailwind-merge. If you prefer not to use the CLI, copy the source from the Manual tab in Installation.