Inline Edit React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React inline edit field for text, numbers and selects that edits in place and saves optimistically.
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.
Inline Edit is a Pro React component in the React form components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add inline-edit, together with UIProvider, Input, Textarea, Number Input, Select and 7 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add inline-editInline Edit is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import InlineEdit from @/components/ui/inline-edit.
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
typedefault"text""text" | "textarea" | "number" | "select"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).
valuestring | number | nullControlled 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.
defaultValuedefault""string | number | nullStarting 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) => voidCalled 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.
optionsSelectItemsThe options of a select editor: { value, label, description?, icon? } or groups of them.
emptyTextdefault"Click to add"stringThe muted display while the value is empty.
placeholderdefault""stringThe editor's placeholder, shown while the draft is empty.
aria-labeldefault""stringNames the display button and the editor. Screen readers hear "Listing title: Trek FX 2 city bike, Edit".
sizedefault"md""sm" | "md" | "lg"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.
variantdefault"ghost""ghost" | "underline"The display: ghost tints on hover like a quiet field, underline draws a dashed line under the text (for values inside a sentence).
radiusdefault"sm""none" | "sm" | "md" | "lg" | "full"Corner shape of the hover tint and the editor box, scaled per size like the button's (sm is 6 / 8 / 10px).
showIcondefaulttruebooleanThe pencil after the value: on hover and keyboard focus with a mouse, always visible on touch.
iconReactNodeReplaces the pencil (and the icon of the edit button with activation="button").
fullWidthdefaulttruebooleanFills 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.
defaultEditingdefaultfalsebooleanStarts in edit mode (without taking focus). Use editing / onEditingChange to control it.
editingbooleanControlled edit mode.
onEditingChange(editing: boolean) => voidCalled when editing starts and ends.
onCancel() => voidCalled after Escape, the cancel button, a select closed without a pick, or a blur with blurAction="cancel".
disableddefaultfalsebooleanDims the value and blocks editing.
readOnlydefaultfalsebooleanShows the value only: not a button, no pencil, text selectable.
activationdefault"click""click" | "doubleclick" | "button"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.
showActionsdefaulttruebooleanSave and cancel buttons after the editor (below a textarea, with the Cmd+Enter hint). Selects save on the pick and never show them.
blurActiondefault"save""save" | "cancel" | "none"What leaving the editor (Tab, a click elsewhere) does. none keeps the editor open until Enter, Escape or a button.
selectOnFocusdefaulttruebooleanSelects the text when the editor opens, so typing replaces it. A textarea puts the caret at the end instead.
maxLengthdefault0numberThe editor's character limit (text and textarea); 0 = none.
successDurationdefault1200numberMilliseconds the saved check stays after a save lands; 0 = no check.
localedefault""stringFormats and parses numbers ("1,200.5" in en-US, "1.200,5" in de-DE). Defaults to the nearest LocaleProvider, then the browser.
formatIntl.NumberFormatOptionsNumber display: { style: "currency", currency: "USD", currencyDisplay: "narrowSymbol" }, units, percent.
renderValue(value) => ReactNodeCustom 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.
editorPropsPartial<InputProps | TextareaProps | NumberInputProps | SelectProps>Extra props for the editor: min / max / step / steppers for numbers, maxRows for a textarea, search for a select.
classNamesPartial<Record<"display" | "value" | "empty" | "icon" | "status" | "editor" | "actions" | "save" | "cancel" | "hint" | "error", string>>Extra classes for the inner parts.
classNamestringClasses for the root, merged last. Font classes here (text-2xl font-semibold) style both the value and the editor.
Parts
InlineEditValue
typeThe value type: string for text, textarea and select; number | null for type="number".
InlineEditLabels
typeThe 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.
Dependencies
Files
- components/ui/inline-edit.tsx
FAQ
Is the React Inline Edit component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Inline Edit?
Run npx wingo-ui@latest add inline-edit. It also adds the Wingo UI items it builds on (UIProvider, Input, Textarea, Number Input, Select and 7 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/inline-edit.
How does Inline Edit behave on phones?
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.
See also
Used in
More React form components
Articles using this component
- React Editable Table: Spreadsheet Grid or Inline Edit
Component guides9 min read
