Source: https://monarchui.dev/components/inline-edit-text/

Components · Forms

# InlineEditText

Text that edits in place: async save, pending and error states, validation.

@rookcom/monarch-react

Component

## Title with validation

## Usage examples

### Failed save keeps the draft

### Multiline with actions

## 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

```tsx
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 unless `saveOnBlur` is false.
- Whitespace is trimmed (`trim`); an unchanged value closes the editor without calling `onSave`.
- `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 optionally `editing` + `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 with `aria-describedby` while it is `aria-invalid`.
- The saving spinner is a named status ("Saving"); the input is `aria-busy` and 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 `label` that 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](https://monarchui.dev/components/text-field/): A plain, always-editable input inside a form.
- [Field](https://monarchui.dev/components/form-field/): Label, help and error wiring for form controls.
- [RecordHeader](https://monarchui.dev/components/record-header/): Planned record header with an inline-edit title.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/), [Spinner](https://monarchui.dev/components/spinner/) |

## 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`

## All example code

### Title with validation

```tsx
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 />
```

### Failed save keeps the draft

```tsx
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

```tsx
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 />
```
