# Inline Edit

> A React inline edit field for text, numbers and selects that edits in place and saves optimistically.

- Type: React component (Forms)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add inline-edit`
- Docs: https://wingo-ui.com/components/inline-edit

## Interaction

Click or tap the value and it becomes an editor of the same size, text selected, with the text in exactly the same place. Enter saves, Escape cancels and puts focus back on the value, Tab or a click elsewhere saves (blurAction). A textarea makes new lines with Enter and saves with Cmd+Enter (Ctrl+Enter on Windows); a select opens at once and saves on the pick, as a bottom sheet on phones. From the keyboard, Tab to the value and press Enter, Space or F2. The new value shows at once with a small spinner, a check pops in when the save lands, and a failed save rolls back, shakes the value and shows the error. On touch the pencil is always visible, the save and cancel buttons get 44px hit areas and the keyboard shows Done.

## Installation

```bash
npx wingo-ui@latest add inline-edit
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/inline-edit` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/inline-edit.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Number Input](https://wingo-ui.com/components/number-input) ([Markdown](https://wingo-ui.com/components/number-input.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))

## Usage

```tsx
import { InlineEdit } from "@/components/ui/inline-edit"

const STATUSES = [
  { value: "active", label: "Active" },
  { value: "reserved", label: "Reserved" },
  { value: "sold", label: "Sold" },
]

export function ListingDetails({ save }: { save: (field: string, value: unknown) => Promise<void> }) {
  return (
    <dl className="grid grid-cols-[96px_1fr] items-center gap-x-4 gap-y-1">
      <dt className="text-sm text-muted-foreground">Title</dt>
      <dd>
        <InlineEdit
          aria-label="Listing title"
          defaultValue="Trek FX 2 city bike"
          emptyText="Add a title"
          maxLength={70}
          validate={(v) => (v.trim() ? null : "The title cannot be empty")}
          onSave={(v) => save("title", v)}
        />
      </dd>
      <dt className="text-sm text-muted-foreground">Price</dt>
      <dd>
        <InlineEdit
          type="number"
          aria-label="Price"
          defaultValue={280}
          locale="en-US"
          format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
          onSave={(v) => save("price", v)}
        />
      </dd>
      <dt className="text-sm text-muted-foreground">Status</dt>
      <dd>
        <InlineEdit type="select" aria-label="Status" options={STATUSES} defaultValue="active" onSave={(v) => save("status", v)} />
      </dd>
    </dl>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `type?` | `"text" \| "textarea" \| "number" \| "select"` | `"text"` | The editor: a single-line Input, a growing Textarea, a NumberInput (value is number \| null, formatted with locale and format) or a Select (needs options, saves on the pick). |
| `value?` | `string \| number \| null` |  | Controlled value: a string, or number \| null for type="number". Pair it with onValueChange, which fires when a save starts (optimistic) and again with the old value if it fails. |
| `defaultValue?` | `string \| number \| null` | `""` | Starting value while uncontrolled: "" for text, null for numbers. The playground reads a number for type="number" and an option value (active, reserved, sold) for select. |
| `onValueChange?` | `(value) => void` |  | Called with the new value when a save starts, and with the previous one when the save fails (rollback). |
| `onSave?` | `(value) => (void \| PromiseLike<unknown>)` |  | Persists the value. A promise keeps the saving spinner; a rejection rolls back, shakes the value and shows the error (its message, else labels.failed). Unchanged values skip it; a save made while one is pending waits and only the latest result counts. |
| `validate?` | `(value) => (string \| null \| undefined)` |  | Return a message to keep the editor open with the error under it. It re-checks while typing once it shows. |
| `options?` | `SelectItems` |  | The options of a select editor: { value, label, description?, icon? } or groups of them. |
| `emptyText?` | `string` | `"Click to add"` | The muted display while the value is empty. |
| `placeholder?` | `string` | `""` | The editor's placeholder, shown while the draft is empty. |
| `aria-label?` | `string` | `""` | Names the display button and the editor. Screen readers hear "Listing title: Trek FX 2 city bike, Edit". |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Text 13 / 14 / 15px (16px on phones) and the editor's field size (32 / 40 / 48px, md 44px on touch). A heading look comes from className (text-2xl font-semibold); the editor follows it. |
| `variant?` | `"ghost" \| "underline"` | `"ghost"` | The display: ghost tints on hover like a quiet field, underline draws a dashed line under the text (for values inside a sentence). |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"sm"` | Corner shape of the hover tint and the editor box, scaled per size like the button's (sm is 6 / 8 / 10px). |
| `showIcon?` | `boolean` | `true` | The pencil after the value: on hover and keyboard focus with a mouse, always visible on touch. |
| `icon?` | `ReactNode` |  | Replaces the pencil (and the icon of the edit button with activation="button"). |
| `fullWidth?` | `boolean` | `true` | Fills its container. false sizes it to the text, so it sits inside a sentence (wrap the sentence in a div, not a <p>: the editor holds block elements); the editor opens as wide as the text was. |
| `defaultEditing?` | `boolean` | `false` | Starts in edit mode (without taking focus). Use editing / onEditingChange to control it. |
| `editing?` | `boolean` |  | Controlled edit mode. |
| `onEditingChange?` | `(editing: boolean) => void` |  | Called when editing starts and ends. |
| `onCancel?` | `() => void` |  | Called after Escape, the cancel button, a select closed without a pick, or a blur with blurAction="cancel". |
| `disabled?` | `boolean` | `false` | Dims the value and blocks editing. |
| `readOnly?` | `boolean` | `false` | Shows the value only: not a button, no pencil, text selectable. |
| `activation?` | `"click" \| "doubleclick" \| "button"` | `"click"` | click: a click or tap edits. doubleclick: a double click edits (a single tap on touch, Enter from the keyboard), for headings people select often. button: only the pencil button edits, so links in the value stay clickable. |
| `showActions?` | `boolean` | `true` | Save and cancel buttons after the editor (below a textarea, with the Cmd+Enter hint). Selects save on the pick and never show them. |
| `blurAction?` | `"save" \| "cancel" \| "none"` | `"save"` | What leaving the editor (Tab, a click elsewhere) does. none keeps the editor open until Enter, Escape or a button. |
| `selectOnFocus?` | `boolean` | `true` | Selects the text when the editor opens, so typing replaces it. A textarea puts the caret at the end instead. |
| `maxLength?` | `number` | `0` | The editor's character limit (text and textarea); 0 = none. |
| `successDuration?` | `number` | `1200` | Milliseconds the saved check stays after a save lands; 0 = no check. |
| `locale?` | `string` | `""` | Formats and parses numbers ("1,200.5" in en-US, "1.200,5" in de-DE). Defaults to the nearest LocaleProvider, then the browser. |
| `format?` | `Intl.NumberFormatOptions` |  | Number display: { style: "currency", currency: "USD", currencyDisplay: "narrowSymbol" }, units, percent. |
| `renderValue?` | `(value) => ReactNode` |  | Custom display, e.g. a price with a badge or a link (pair it with activation="button"). |
| `labels?` | `{ edit?; save?; cancel?; saving?; saved?; failed?; saveHint? }` |  | Every string: { edit: "Edit", save: "Save", cancel: "Cancel", saving: "Saving", saved: "Saved", failed: "Could not save", saveHint: "to save" }. Saved and failed are announced in a polite live region. |
| `editorProps?` | `Partial<InputProps \| TextareaProps \| NumberInputProps \| SelectProps>` |  | Extra props for the editor: min / max / step / steppers for numbers, maxRows for a textarea, search for a select. |
| `classNames?` | `Partial<Record<"display" \| "value" \| "empty" \| "icon" \| "status" \| "editor" \| "actions" \| "save" \| "cancel" \| "hint" \| "error", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Classes for the root, merged last. Font classes here (text-2xl font-semibold) style both the value and the editor. |

## Parts

### InlineEditValue (type)

The value type: string for text, textarea and select; number | null for type="number".

### InlineEditLabels (type)

The labels object: edit, save, cancel, saving, saved, failed and saveHint.

## Examples

- **Editable price cells**: A table where there is room and cards on phones (a container query), both controlled by the same state: edit a price, the total follows, a check pops when the save lands.
- **Heading with double click**: activation="doubleclick" and a heading look from className: the editor takes the same 24px semibold type, so the text never moves.
- **Values with links**: activation="button": the link stays clickable and only the pencil starts editing. renderValue draws the link.
- **Validation**: validate keeps the editor open with the message under it and re-checks while you type.
- **Optimistic save, rollback**: The new price shows at once with a spinner; the request fails, the old value comes back, the price shakes and the error explains why.
- **Inside a sentence**: variant="underline" with fullWidth={false}: dashed values in running text, Enter to save, Escape to cancel.
- **Sizes and locales**: sm, md and lg rows formatting en-US dollars, de-DE euros and ro-RO lei; the editor parses each locale's decimals.

## See also

- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Number Input](https://wingo-ui.com/components/number-input) ([Markdown](https://wingo-ui.com/components/number-input.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Editable Grid](https://wingo-ui.com/components/editable-grid) ([Markdown](https://wingo-ui.com/components/editable-grid.md))
- [Description List](https://wingo-ui.com/components/description-list) ([Markdown](https://wingo-ui.com/components/description-list.md))

---

Canonical: https://wingo-ui.com/components/inline-edit
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
