useInlineEdit

useInlineEdit({ value, onCommit, onCancel }) edits a value in place, such as a file's name. It returns whether the edit is open, the draft, and props for the button that starts the edit, the group that holds the input and its buttons, and the input.

On this page
const nameId = useId();
const edit = useInlineEdit({ value: name, onCommit: setName });

{edit.editing ? (
  <Stack.Root {...edit.getGroupProps()} direction="row" gap="condensed" align="center">
    <Field.Root aria-label="Name">
      <Field.TextInput {...edit.getInputProps()} />
    </Field.Root>
    <Button.Root onClick={edit.commit}>Save</Button.Root>
    <Button.Root variant="secondary" onClick={edit.cancel}>
      Cancel
    </Button.Root>
  </Stack.Root>
) : (
  <Stack.Root direction="row" gap="condensed" align="center">
    <Text.Root id={nameId}>{name}</Text.Root>
    <Button.Root {...edit.getTriggerProps()} variant="secondary" aria-describedby={nameId}>
      Rename
    </Button.Root>
  </Stack.Root>
)}

Press Rename, type a new name, then press Enter to save or Escape to put the name back. Clear the name to see an edit that is turned down.

Q3 report

start(), which the trigger calls, opens the edit from value, focuses the input and selects its text, so typing replaces the name. start(draft) opens it with another text, such as a name a failed save should offer again. Neither does anything while the edit is open.

Enter and focus leaving the group save the draft through onCommit. Return false from it to keep the edit open, such as for an empty name. When focus has left the edit, Escape can't reach it any more, so false puts the value back and calls onCancel instead. A draft equal to value closes the edit without calling onCommit. Escape cancels, and calls onCancel. An Enter or Escape pressed during a Korean or Japanese composition is left to the input. Safari can send the Escape that ends a composition after the composition, and then it cancels the edit, as Escape does in Todae's other fields. Escape in an edit inside a Dialog or a Drawer cancels the edit, and the dialog stays open; the next Escape closes it.

When a save or a cancel closes the edit with focus inside it, focus returns to the trigger once it is back in the page. An edit that closes because focus moved elsewhere leaves focus there. A tab or window switch keeps the edit open.

Accessible composition

Todae handles: Focusing the input and selecting its text. Enter and Escape, with IME compositions left alone. Escape and the back gesture in a modal reaching the edit first. Saving when focus leaves the group, but not when the window loses focus. Keeping focus in the input while anything in the group but a text field, select, slider or editable element is pressed, so a mouse press on Cancel cancels in Safari too. Returning focus to the trigger.

You supply: A name for the input and for each icon-only button. The trigger, the group and the input, in their places in your markup. An error message tied to the input with aria-describedby when you turn a draft down.

Watch for: A trigger whose name doesn't say what it edits: describe it by the value, as the example does, so the new value is read when focus returns. Turning a draft down without showing why. A Save or Cancel button outside the group: focus moving to it from the input saves before its click. A list keyed by the value: a rename gives the row a new key, React mounts a new row, and focus has nowhere to return. Key rows by an id. A control in the group whose popup renders elsewhere in the page, such as in a portal: focus moving into the popup leaves the group and saves.

When to use

When: A short value a person changes where it is shown, such as the name of a file, a list or a row.

When not: Text over several lines, or several values that change together → a Form, in a Dialog when it doesn't fit on the page. The whole pattern drawn with Todae's parts → the Inline edit recipe.

Props

useInlineEdit props
NameTypeDescription
value Requiredstring

The saved value. Each edit starts from it.

onCommit Required(draft: string) => boolean | void

Called with the draft when the person saves: Enter, a Save button that calls commit, or focus leaving the edit. Return false to keep editing, such as for an empty name; when focus left the edit, false puts the value back and calls onCancel. Not called when the draft equals value; the edit just closes.

onCancel() => void

Called when the person cancels: Escape, or a Cancel button that calls cancel. Also called when focus leaves the edit with a draft onCommit turns down.