Text that becomes an input where it sits — the box climbs a rung on entering edit and drops back on commit. Elevation is the edit state, and the box change is a small-scale morph.
Project
pnpm dlx shadcn@latest add https://matos-ui.com/r/edit-in-place.jsonimport { EditInPlace } from '@/components/matos-ui/edit-in-place'const [name, setName] = useState('Quarterly planning')
<EditInPlace label="Project name" value={name} onValueChange={setName} />A click — or Enter, Space, F2 — enters edit.
Enter commits, Esc reverts, and leaving the field commits
(submitOnBlur, on by default).
At rest the box is flush with its substrate — no fill, no ring — and reads as plain text. On edit it climbs one rung up the elevation ladder, grows its padding, rounds its corners and draws a border. Committing undoes all of it.
The whole transition is spring.morph, the shape tier — the same one a bento
uses across hundreds of pixels, here across a handful. It is the argument that
the tier holds at both scales.
renderValue included), so the width always matches.layout + layoutDependency={editing} animates the box only on the mode
switch — typing changes the sizer width without triggering the morph.ease.anticipate, the
12-principles anticipation beat, with a beat of delay so it enters after the
box has settled.prefers-reduced-motion keeps the crossfade and the colour shift, and
drops the box scale, the travel and the anticipation dip.validate is given the value and returns true to accept or a string to
reject — the string shows below the field, the box does an attentionShake, and
editing stays open.
<EditInPlace
label="URL slug"
value={slug}
onValueChange={setSlug}
validate={(v) =>
/^[a-z0-9-]+$/.test(v) || 'Lowercase letters, numbers and dashes only.'
}
/><button aria-label="Edit …">; on edit, an <input> with the
same accessible name, focused and with its text selected.aria-invalid and wires aria-describedby to the message (role="alert").| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The editable value. |
onValueChange | (value: string) => void | — | Commit — Enter, the ✓ key, or blur. |
onCancel | () => void | — | Fires on Escape or the ✕ key, after the value is reverted. |
label | string | — | Accessible name for the field. Required. |
placeholder | string | "Empty" | Shown when the value is empty. |
validate | (value: string) => boolean | string | — | A string rejects the commit and becomes the error message. |
submitOnBlur | boolean | true | Leaving the field commits; false reverts. |
selectOnEdit | boolean | true | Select the text when editing starts. |
trim | boolean | true | Trim surrounding whitespace on commit. |
maxLength | number | — | — |
renderValue | (value: string) => ReactNode | — | Format the resting display (the editable string is value). |
disabled | boolean | false | — |
size | "sm" | "md" | "lg" | "md" | — |
editing | boolean | — | Controlled edit state. |
defaultEditing | boolean | false | — |
onEditingChange | (editing: boolean) => void | — | — |
name | string | — | Name for a hidden input, so the value posts with a form. |
Install Matos UI
Choose a package manager and copy one command for every component.