Components · Forms
InlineEditText
Text that edits in place: async save, pending and error states, validation.
Title with validation
import { useState } from 'react';
import { InlineEditText } from '@rookcom/monarch-react';
function wait(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
}
function TitleDemo() {
const [title, setTitle] = useState('Update onboarding checklist');
return (
<div style={{ fontSize: '1.25rem', fontWeight: 600, width: '100%', maxWidth: 480 }}>
<InlineEditText
label="Title"
value={title}
maxLength={120}
validate={(v) => (v.length === 0 ? 'Enter a title.' : null)}
onSave={async (next) => {
await wait(600);
setTitle(next);
}}
/>
</div>
);
}
<TitleDemo />Usage examples
Failed save keeps the draft
import { InlineEditText } from '@rookcom/monarch-react';
function wait(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
}
function FailingDemo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<InlineEditText
label="Project name"
defaultValue="Website refresh"
onSave={async () => {
await wait(600);
throw new Error('The name could not be saved. Check your connection and try again.');
}}
/>
</div>
);
}
<FailingDemo />Multiline with actions
import { useState } from 'react';
import { InlineEditText } from '@rookcom/monarch-react';
function wait(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
}
function DescriptionDemo() {
const [text, setText] = useState('');
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<InlineEditText
label="Description"
multiline
placeholder="Add a description"
value={text}
onSave={async (next) => {
await wait(400);
setText(next);
}}
/>
</div>
);
}
<DescriptionDemo />Purpose
Lets people rename or rewrite a short piece of text where it is shown, such as a record title or a description, without opening a form. The product supplies persistence; the component handles the edit cycle and its states.
Use when
- A single text value on a detail page should be editable in place: an issue or ticket title, a category name, a description.
- The save is per field and can fail, so the user needs a pending state and an error that keeps their draft.
Avoid when
- Several fields are edited and saved together: use a form with Field and an explicit Save.
- The value is not text (dates, people, options): use the matching picker.
- Rich text with formatting or mentions: that editor stays in the product.
Design principles
- The text looks like text until it is hovered or focused; a pencil icon and a hover surface reveal that it is editable.
- View and edit modes share padding and border width, so switching does not move the text. Font size and weight are inherited, so it fits a page title or a table cell.
- A failed save never loses work: the editor stays open with the draft and the error.
- No business logic: saving, validation rules and conflict handling are callbacks from the product.
API reference
import { InlineEditText } from '@rookcom/monarch-react';| Export | Kind | Rendering | Root class |
|---|---|---|---|
InlineEditText | component | client | .mn-inline-edit- |
Renders <div class="mn-inline-edit-text"> (ref) containing a <button> in view mode, or an <input> (<textarea> with multiline) in edit mode.
onSave(next) may return a promise. While it is pending the input is read-only and a spinner shows; if it rejects, the editor stays open and getErrorMessage(error) is shown.
Edit conflicts stay in the product: handle them in onSaveError(error, draft) and pass a message (or your own resolution UI) through error while keeping editing true.
Generated from the TypeScript declarations of packages/react/src/components/forms/inline-edit-text.
Properties
InlineEditText
Text that turns into an input in place: click it or press Enter to edit, Enter (or leaving the field) saves, Escape cancels. Saving can be async, with a pending state, and a failed save keeps the editor open with the error. The product owns persistence and conflict handling.
| Prop | Type | Description |
|---|---|---|
label required | string | What the text is, for example "Title". The input's accessible name, and part of the view button's. |
onSave required | (next: string) => void | Promise<void> | Persists a new value. Return a promise to show the pending state; reject it (throw) to keep the editor open with the error and the user's draft. Not called when the value is unchanged. |
cancelLabel | string | |
className | string | |
defaultEditing | boolean | Initial edit mode (uncontrolled). |
defaultValue | string | The initial saved value (uncontrolled). |
disabled | boolean | Not editable: the value is shown as plain text. |
editHint | string | Visually hidden hint read after the view button's name. Default "Edit". |
editing | boolean | Edit mode (controlled). |
error | ReactNode | An error supplied by the product (for example a conflict notice), shown while editing. |
getErrorMessage | ((error: unknown) => ReactNode) | Turns a rejected save into a message. Default: the Error's message, or "Couldn't save. Try again." |
id | string | |
maxLength | number | |
multiline | boolean | A textarea instead of an input: Enter adds a line, Ctrl/⌘+Enter saves. |
onCancel | (() => void) | Called when the user cancels with Escape or the Cancel button. |
onEditingChange | ((editing: boolean) => void) | |
onSaveError | ((error: unknown, draft: string) => void) | Called when `onSave` rejects, with the error and the draft that failed. Use it for product-specific handling such as an edit conflict (HTTP 409): keep the editor open and pass your own resolution UI through `error`. |
placeholder | string | Shown in view mode when the value is empty, and as the input placeholder. |
rows | number | Rows for `multiline`. Default 3. |
saveLabel | string | |
saveOnBlur | boolean | Save when focus leaves the editor. Default true. |
savingLabel | string | Accessible name of the pending spinner. Default "Saving". |
showActions | boolean | Show Save and Cancel buttons under the editor. Default: true for `multiline`. |
trim | boolean | Trim whitespace before comparing and saving. Default true. |
validate | ((draft: string) => ReactNode | null | undefined) | Validates the draft before saving. Return a message to block the save, or nothing to allow it. |
value | string | The saved value (controlled). |
Variants
| Prop | Values | Description |
|---|---|---|
multiline | 'true''false' | A textarea that grows with its content (field-sizing: content where supported). Enter adds a line; Ctrl/⌘+Enter saves. Shows Save and Cancel buttons by default (data-multiline). |
showActions | 'true''false' | Save and Cancel buttons under the editor. Default true for multiline. |
saveOnBlur | 'true''false' | Save when focus leaves the editor (default). Turn off when only explicit saves should count. |
States
| State | Exposed as | Behaviour |
|---|---|---|
| Viewing | data-state="viewing" | A button showing the value, or the placeholder in subtle text when empty (data-empty). |
| Editing | data-state="editing" | An input with the draft, focused with its text selected. |
| Saving | data-saving, aria-busy="true", readonly | The input is read-only with a spinner named "Saving"; with actions, the Save button shows its loading state. |
| Error | data-invalid, aria-invalid="true" | A validation message, a failed save, or the product’s error. Shown under the input with role="alert" and linked by aria-describedby. |
| Disabled | data-disabled | Plain text, not a button: nothing to activate. |
Interaction behaviour
- Click the text, or focus it and press Enter or Space, to edit.
- Enter saves (Ctrl/⌘+Enter with
multiline); Escape cancels and restores the saved value; leaving the field saves unlesssaveOnBluris false. - Whitespace is trimmed (
trim); an unchanged value closes the editor without callingonSave. validate(draft)runs before saving; a message blocks the save and keeps the editor open.- A failed save keeps the draft and the error. Leaving the field does not resend the same failed draft; editing it, Enter or Save tries again.
- After Enter, Escape, Save or Cancel, focus returns to the text. After a blur save, focus stays where the user moved it.
- Controlled:
value+onSave, and optionallyediting+onEditingChange. Uncontrolled:defaultValue,defaultEditing.
Keyboard support
| Keys | Action |
|---|---|
| Tab | Moves to the text (a button), or through the input and its buttons. |
| Enter / Space | On the text: starts editing. |
| Enter | In a single-line editor: saves. |
| Ctrl/⌘ + Enter | In a multiline editor: saves. |
| Escape | Cancels the edit and returns focus to the text. |
Enter is ignored while an input method editor is composing text.
Accessibility
- Role
- button (view), textbox (edit)
- Accessible name
label: the input is named by it, and the view button reads "{label}: {value}" with an "Edit" description (editHint).
- Edit mode is not hidden behind hover: the text itself is the keyboard-reachable control.
- Errors use
role="alert"and are linked to the input witharia-describedbywhile it isaria-invalid. - The saving spinner is a named status ("Saving"); the input is
aria-busyand read-only, not disabled, so focus stays in place. - The pencil icon is decorative.
Responsive behaviour
- Fills its container and wraps long values (
overflow-wrap: anywhere); no horizontal scrolling at 320px. - On touch devices (
hover: none) the pencil icon is always visible, since there is no hover to reveal it. - Action buttons wrap under the editor on narrow screens.
Theme support
- Surfaces, borders and text use Monarch semantic tokens, so light, dark and more-contrast themes apply automatically.
- The focus ring uses
--mn-color-focus-ring(the product accent); errors use--mn-color-danger-strong. - Transitions use the duration tokens, which are zero under
prefers-reduced-motion.
Do and don't
Do
- Give every instance a
labelthat names the value ("Title", "Category name"). - Return the save promise from
onSave, so the pending and error states are real. - Throw an Error with a message that says what to do next.
- Keep conflict detection in the product and show it through
error.
Don't
- Do not use it for values that must be saved together with other fields.
- Do not swallow save errors and resolve the promise: the user would believe it saved.
- Do not hide the only edit affordance behind hover; the text itself is the control.
Related components
- Input: A plain, always-editable input inside a form.
- Field: Label, help and error wiring for form controls.
- RecordHeader: Planned record header with an inline-edit title.
Dependencies
Testing coverage
- View mode is a button named "{label}: {value}" with an "Edit" description.
- Click to edit focuses the input; Enter saves trimmed text and returns focus (uncontrolled).
- Enter on the text starts editing; Escape cancels, calls onCancel and restores focus.
- Blur saves without stealing focus back; an unchanged value does not call onSave.
- Async save: aria-busy, read-only and a "Saving" status; on rejection the draft, aria-invalid, the alert and focus are kept, and onSaveError gets the error and draft.
- A failed draft is not resent on blur, but is retried after an edit.
- validate blocks the save and describes the input.
- Controlled value and editing follow the parent; an external error shows while editing.
- Multiline: Enter adds a line, Ctrl+Enter saves, Save and Cancel buttons work; tabbing to the buttons does not save.
- Disabled renders plain text; an empty value shows the placeholder; server rendering in both modes.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery; Cross-browser axe: /components/inline-edit-text |
Tests: packages/react/src/components/forms/inline-edit-text