Inline edit
Rename in place: the text becomes a field without moving.
import { InlineEdit } from "@sagui/ui";Loading demo
When to use
- Titles and descriptions read far more often than edited, like a project name.
- Single fields that save on their own with optimistic updates and rollback.
When not to use
- Use input or textarea in a form when several fields save together.
- Use input when the field should always look editable.
Installation
Install the package and import the styles once. The installation guide covers the Tailwind setup.
$ npm install @sagui/uiUsage
import { InlineEdit } from "@sagui/ui";
export function ProjectTitle({ project }: { project: { id: string; name: string } }) {
return (
<InlineEdit
as="h1"
label="Project name"
value={project.name}
validate={(next) => (next.trim() ? null : "Name can’t be empty")}
onSave={(next) => renameProject(project.id, next)}
/>
);
}Examples
Body text
Loading demo
Multiline
Loading demo
Validation
Return a message from validate to block a save. The message opens under the field and the draft stays open.
Loading demo
When a save fails
Reject onSave to roll back. The last saved text returns and a message offers to try again.
Loading demo
API reference
InlineEdit
Click-to-edit text that becomes a field in place with the same metrics, saves optimistically, and rolls back on failure.
| Prop | Type | Default | Description |
|---|---|---|---|
value (required) |
string |
– | The saved value. Outside changes replace the text while not editing. |
onSave (required) |
(next: string) => void | Promise<unknown> |
– | Persists the value. Return a promise to show saving; reject it to roll back with a retry. |
label (required) |
string |
– | Accessible name, for example "Project name". |
validate |
(next: string) => string | null | undefined |
– | Returns an error message to block saving. |
placeholder |
string |
"" |
Shown when the value is empty. |
multiline |
boolean |
false |
Wraps and grows in height; Shift+Enter adds a line break. |
variant |
"title" | "body" |
"title" |
Type style: title for names and headings, body for descriptions. |
as |
"span" | "p" | "h1" | "h2" | "h3" |
"span" |
Element that holds the text, so a title can stay a heading. |
className |
string |
– | Added to the root. |
Keyboard interactions
| Keys | Action |
|---|---|
| EnterorSpace | On the text, starts editing. |
| Enter | Saves the draft. In multiline mode, Shift+Enter inserts a line break. |
| Escape | Cancels and rolls the text back. |
| Tab | Leaving the component saves, the way a rename does. |
Accessibility
- The resting text is a native button labelled ": ", so it is focusable and announced as editable.
- The field has aria-label and aria-invalid; validation and save errors render in a polite live region linked through aria-describedby.
- Save progress and results are announced through a role="status" region; save and cancel buttons are labelled.
Motion
- Text rolls between values, a check draws once a save lands, and multiline boxes follow their height; failed saves roll back with motion.
- Reduced motion crossfades text, draws the check instantly, and replaces the spinner with a static mark.
Responsive behavior
- Text wraps within the column and reserves 66px at the end for the save and cancel buttons.
- With multiline the box grows in height as the text wraps.
- Hover hints apply only on fine pointers; on touch a tap starts editing.
Performance
- Two ResizeObservers size the frame; sizes are measured in a layout effect, so no frame shows a wrong width.
Notes
- Use for single fields read far more often than edited, like titles and descriptions. Use a regular form with input or textarea when several fields save together.
- value is controlled by the saved data; update it after onSave resolves. Return a promise from onSave to get saving, saved, and failed states.
- Exported as both named and default.