Inline Edit
betaA value people change where it sits. The value is the button, and the field opens in its exact box.
Preview
size
empty
saving
fails
disabled
- Owner
- Due
- Notes
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/inline-edit
Use it for
- One short value on a page or a row that people change now and then: a name, a title, a place, a number.
- Pass label as a short noun, like "location". It names the value "Edit location" and the empty state "Add location".
- Save in onSave. Return the promise from your request, and the change shows at once, says Saving…, and comes back if it fails.
- When you control value, set it to the new text before your promise resolves.
Avoid it for
- Text that runs to several lines, like a description. Use a Textarea in a form or a dialog.
- Values that must change together, like a start and an end date. Edit them in one form with one Save.
- A form people fill in once from top to bottom. Use Input with a label.
- Adding a pencil or an Edit button beside the value. The value is already the button.
Props
- label
What the value is, as a short noun. Required. It names the value and the empty state.
string
- value
The saved value. Controlled. Set it to the new text before your onSave promise resolves.
string
- defaultValue
The saved value, when you do not control value.
string · default ""
- onSave
Called with the trimmed new text when it changed. Enter saves, and so does clicking away. Escape cancels. A promise shows Saving… until it settles. A rejection or a throw puts the old value back and shows errorText. The newest save wins.
(value: string) => void | Promise<unknown>
- size
Text size: 11, 13, or 16px. The box is as tall as a small control, so density scales it.
"sm" | "md" | "lg" · default "md"
- disabled
Shows the value as plain text. It is not a button and cannot be edited.
boolean · default false
- placeholder
Shown in the empty field while editing.
string
- editText
Words before the value in its name for screen readers. Set it to translate.
string · default "Edit {label}"
- addText
Shown with a plus when the value is empty. Set it to translate.
string · default "Add {label}"
- savingText
Shown after the value while its save runs.
string · default "Saving…"
- errorText
Shown under the value when a save fails.
string · default "Could not save. Try again."
- className
Extra classes on the outer span. Later classes win, so className='text-base' replaces the size.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| label | string | - | What the value is, as a short noun. Required. It names the value and the empty state. |
| value | string | - | The saved value. Controlled. Set it to the new text before your onSave promise resolves. |
| defaultValue | string | "" | The saved value, when you do not control value. |
| onSave | (value: string) => void | Promise<unknown> | - | Called with the trimmed new text when it changed. Enter saves, and so does clicking away. Escape cancels. A promise shows Saving… until it settles. A rejection or a throw puts the old value back and shows errorText. The newest save wins. |
| size | "sm" | "md" | "lg" | "md" | Text size: 11, 13, or 16px. The box is as tall as a small control, so density scales it. |
| disabled | boolean | false | Shows the value as plain text. It is not a button and cannot be edited. |
| placeholder | string | - | Shown in the empty field while editing. |
| editText | string | "Edit {label}" | Words before the value in its name for screen readers. Set it to translate. |
| addText | string | "Add {label}" | Shown with a plus when the value is empty. Set it to translate. |
| savingText | string | "Saving…" | Shown after the value while its save runs. |
| errorText | string | "Could not save. Try again." | Shown under the value when a save fails. |
| className | string | - | Extra classes on the outer span. Later classes win, so className='text-base' replaces the size. |
Accessibility
- The value is a native <button> named "Edit {label}: {value}", so screen readers hear what it is and what it holds. An empty value is named "Add {label}".
- Enter, Space, or a click opens a text field with the value, focused. A short value is selected, so typing replaces it.
- Enter saves and Escape cancels. Both put focus back on the value. Clicking away saves and leaves focus where you clicked.
- Escape stops at the field, so a dialog or popover around it stays open.
- While a save runs, the value button is described by the Saving… text. A failed save shows its message with role="alert" and describes the button.
- The field takes the value's exact box, so nothing on the page moves when it opens or closes.
- On touch screens the text is 16px, so iOS Safari does not zoom in when the field opens.
- A disabled value is plain text with no button, so it does not look like something to press.
Examples
Save to your server
<InlineEdit
label="location"
defaultValue="Berlin"
onSave={(location) => fetch("/api/profile", { method: "PATCH", body: JSON.stringify({ location }) }).then((r) => {
if (!r.ok) throw new Error("Save failed");
})}
/>Controlled
const [title, setTitle] = useState("Q3 planning");
<InlineEdit
label="title"
size="lg"
value={title}
onSave={async (next) => {
await saveTitle(next);
setTitle(next);
}}
/>Empty, in a table cell (needs @txbrendan/table)
<TableCell>
<InlineEdit label="owner" placeholder="Name or email" onSave={setOwner} />
</TableCell>