Editable
Overview
An inline text editor that switches between preview and edit states. In preview mode the value is shown as a focusable text node (or a placeholder); focusing, double-clicking, or clicking the edit button enters edit mode, then submitting (Enter/blur/submit trigger) or cancelling (Esc/cancel trigger) returns to preview. The structure follows the Aria core + styled wrapper split: the 8 EditableRoot-family components in @vean/aria (zero styles) own the full state machine, and SEditable injects the editableVariants styles (8 slots: root/area/preview/input/controls/editTrigger/submitTrigger/cancelTrigger).
Usage
Features
- 🔀 Two-state machine — preview/edit switching with
update:statereportingedit/submit/cancel - 🖱️ Activation modes —
activationMode:focus(focus to edit, default),dblclick(double-click to edit),none(edit only via the edit trigger oredit()) - ⌨️ Submit modes —
submitMode:blur(submit on blur, default),enter(submit on Enter),both,none(blur cancels only); Esc always cancels - 🔁 Controlled/uncontrolled —
modelValue+update:modelValuefor controlled,defaultValuefor uncontrolled; thesubmitevent carries the submitted value - 🎛️ Editing UX —
selectOnFocus(select all on focus),startWithEditMode(enter edit on mount),maxLength,autoResize(inline-grid width) - 🧩 Aria/styled split —
EditableCompactaggregates the 8 primitives and exposes 6*Propschannels (area/preview/input/editTrigger/submitTrigger/cancelTrigger) - ♿ Accessible by default — focusable preview (
tabindex="0"), full keyboard support (Enter submits / Esc cancels), localized triggeraria-labels, and a hidden input proxy (VisuallyHiddenInput) for forms - 🎨 8-slot styling — root/area/preview/input/controls/editTrigger/submitTrigger/cancelTrigger plus
sizevariants (xs–2xl)
Component family
SEditable(styled) — the entry wrapper; puredefineProps+editableVariantswith dynamic 8-slot forwarding anduseForwardListenersevent mergingEditableRoot(Aria) — the state-machine owner: holdsmodelValue/inputValue/isEditing/placeholder/isEmpty, exposesedit/cancel/submit; rendersdata-dismissable-layerfor blur/outside-click handling; inside a.formcontainer with anameit renders the hidden input proxyEditableArea(Aria) — the container; uses an inline-grid layout whenautoResizeis onEditablePreview(Aria) — the preview text;tabindex = disabled ? -1 : 0, activated on focusin/dblclickEditableInput(Aria) — the real input; activate/cancel/submit keyboard handling andselectOnFocusselectionEditableEditTrigger/EditableSubmitTrigger/EditableCancelTrigger(Aria) —Button-based triggers;aria-labelfalls back touseLocaleMessageslocalized messages; disabled whendisabled || readonlyEditableCompact(Aria) — the aggregated composite of root/area/preview/input and the three triggers; slot props providemodelValue/inputValue/isEditing/isEmpty/edit/cancel/submit
Demos
Basic
Value: Click to edit your display name
State: preview
Custom Styling
- 01 Basic — controlled
v-model+ placeholder withsubmit/update:stateevents - 02 Size —
sizevariants - 03 Disabled —
disabled - 04 Activation mode —
focusvsdblclick - 05 Custom styling — 8-slot
uioverrides
API
Aria Layer
9Editable
Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | ClassValue | — | Additional class names applied to the root element. |
size | ThemeSize | — | Visual size of the component. |
ui | Partial<EditableUi> | — | Per-slot class overrides for the component. |
areaProps | EditableAreaProps | — | Properties forwarded to the area element. |
previewProps | EditablePreviewProps | — | Properties forwarded to the preview element. |
inputProps | EditableInputProps | — | Properties forwarded to the input element. |
editTriggerProps | EditableEditTriggerProps | — | Properties forwarded to the edit trigger element. |
submitTriggerProps | EditableSubmitTriggerProps | — | Properties forwarded to the submit trigger element. |
cancelTriggerProps | EditableCancelTriggerProps | — | Properties forwarded to the cancel trigger element. |
defaultValue | string | — | The default value of the editable field. |
modelValue | string | — | The controlled value of the editable field. |
placeholder | string | EditablePlaceholder | — | The placeholder for the editable field. |
dir | Direction | — | The reading direction of the editable field when applicable. |
disabled | boolean | — | When `true`, prevents the user from interacting with the editable field. |
readonly | boolean | — | When `true`, prevents the user from editing the value. |
activationMode | EditableActivationMode | — | The activation event of the editable field. |
selectOnFocus | boolean | — | Whether to select the text in the input when it is focused. |
submitMode | EditableSubmitMode | — | The submit event of the editable field. |
startWithEditMode | boolean | — | Whether to start with the edit mode active. |
maxLength | number | — | The maximum number of characters allowed. |
autoResize | boolean | — | Whether the editable field should auto resize. |
id | string | — | The id of the field. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string | Event handler called whenever the model value changes. |
submit | string | Event handler called when a value is submitted. |
update:state | EditableEventState | Event handler called when the editable field changes state. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | EditableCompactSlotProps | Custom content for the default slot. |
preview | EditableCompactSlotProps | Custom content for the preview slot. |
input | EditableCompactSlotProps | Custom content for the input slot. |
edit-trigger | EditableCompactSlotProps | Custom content for the edit trigger slot. |
submit-trigger | EditableCompactSlotProps | Custom content for the submit trigger slot. |
cancel-trigger | EditableCompactSlotProps | Custom content for the cancel trigger slot. |
EditableArea
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
EditableCancelTrigger
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
EditableCompact
Props
| Prop | Type | Default | Description |
|---|---|---|---|
areaProps | EditableAreaProps | — | Properties forwarded to the area element. |
previewProps | EditablePreviewProps | — | Properties forwarded to the preview element. |
inputProps | EditableInputProps | — | Properties forwarded to the input element. |
editTriggerProps | EditableEditTriggerProps | — | Properties forwarded to the edit trigger element. |
submitTriggerProps | EditableSubmitTriggerProps | — | Properties forwarded to the submit trigger element. |
cancelTriggerProps | EditableCancelTriggerProps | — | Properties forwarded to the cancel trigger element. |
defaultValue | string | — | The default value of the editable field. |
modelValue | string | — | The controlled value of the editable field. |
placeholder | string | EditablePlaceholder | — | The placeholder for the editable field. |
dir | Direction | — | The reading direction of the editable field when applicable. |
disabled | boolean | — | When `true`, prevents the user from interacting with the editable field. |
readonly | boolean | — | When `true`, prevents the user from editing the value. |
activationMode | EditableActivationMode | — | The activation event of the editable field. |
selectOnFocus | boolean | — | Whether to select the text in the input when it is focused. |
submitMode | EditableSubmitMode | — | The submit event of the editable field. |
startWithEditMode | boolean | — | Whether to start with the edit mode active. |
maxLength | number | — | The maximum number of characters allowed. |
autoResize | boolean | — | Whether the editable field should auto resize. |
id | string | — | The id of the field. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string | Event handler called whenever the model value changes. |
submit | string | Event handler called when a value is submitted. |
update:state | EditableEventState | Event handler called when the editable field changes state. |
Slots
| Slot Name | Parameters | Description |
|---|---|---|
default | EditableCompactSlotProps | Custom content for the default slot. |
preview | EditableCompactSlotProps | Custom content for the preview slot. |
input | EditableCompactSlotProps | Custom content for the input slot. |
edit-trigger | EditableCompactSlotProps | Custom content for the edit trigger slot. |
submit-trigger | EditableCompactSlotProps | Custom content for the submit trigger slot. |
cancel-trigger | EditableCompactSlotProps | Custom content for the cancel trigger slot. |
EditableEditTrigger
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
EditableInput
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Id. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
EditablePreview
Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
EditableRoot
Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | string | — | The default value of the editable field. |
modelValue | string | — | The controlled value of the editable field. |
placeholder | string | EditablePlaceholder | — | The placeholder for the editable field. |
dir | Direction | — | The reading direction of the editable field when applicable. |
disabled | boolean | — | When `true`, prevents the user from interacting with the editable field. |
readonly | boolean | — | When `true`, prevents the user from editing the value. |
activationMode | EditableActivationMode | — | The activation event of the editable field. |
selectOnFocus | boolean | — | Whether to select the text in the input when it is focused. |
submitMode | EditableSubmitMode | — | The submit event of the editable field. |
startWithEditMode | boolean | — | Whether to start with the edit mode active. |
maxLength | number | — | The maximum number of characters allowed. |
autoResize | boolean | — | Whether the editable field should auto resize. |
id | string | — | The id of the field. |
name | string | — | The name of the field. Submitted with its owning form as part of a name/value pair. |
required | boolean | — | When `true`, indicates that the user must set the value before the owning form can be submitted. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Emits
| Emit Name | Parameters | Description |
|---|---|---|
update:modelValue | string | Event handler called whenever the model value changes. |
submit | string | Event handler called when a value is submitted. |
update:state | EditableEventState | Event handler called when the editable field changes state. |
EditableSubmitTrigger
Props
| Prop | Type | Default | Description |
|---|---|---|---|
type | ButtonType | 'button' | The type of the button element. Can be one of 'button', 'submit', or 'reset'. |
disabled | boolean | — | Whether the component is disabled. |
asChild | boolean | — | Change the default rendered element for the one passed as a child, merging their props and behavior. |
as | AsTag | Component | 'div' | The element or component this component should render as. Can be overwrite by `asChild` |
Notes
Architecture and benchmark differences
EditableRoot uses useControllableState to unify controlled/uncontrolled; inputValue (the edit buffer) is separate from modelValue (the committed value), and currentValue picks one based on the state. While editing, an external modelValue push does not overwrite the in-progress input (it is written back only on submit/cancel), so a controlled parent cannot stomp the user’s typing. Activation (focus/dblclick/edit trigger) and submission (blur/enter/submit trigger) are decoupled both ways; useFocusOutside/usePointerdownOutside gate on isEditing to implement submit-on-blur. autoResize uses an inline-grid + grid-template-columns: auto layout so the input hugs its content. The API is co-designed with reka-ui’s Editable (activation/submit modes, selectOnFocus, startWithEditMode, and the form proxy are all present); Ant Design’s Typography editable only offers button activation plus a controlled editing flag; Element Plus and shadcn/ui have no equivalent component; Mantine’s EditableText covers a subset of the activation/submit behavior.
| Capability | VeanUI | reka-ui | Ant Design Typography | Mantine EditableText | Element Plus | shadcn/ui |
|---|---|---|---|---|---|---|
| Aria/styled split | ✅ | ✅ | — | — | — | — |
| Activation mode (focus/dblclick/none) | ✅ | ✅ | — | ⚠️ | — | — |
| Submit mode (blur/enter/none/both) | ✅ | ✅ | — | ⚠️ | — | — |
| Esc to cancel | ✅ | ✅ | ⚠️ | ✅ | — | — |
selectOnFocus | ✅ | ✅ | — | ✅ | — | — |
startWithEditMode | ✅ | ✅ | ⚠️ | ✅ | — | — |
autoResize (width fits content) | ✅ | ✅ | — | — | — | — |
Form proxy (name + form hidden input) | ✅ | ✅ | — | — | — | — |
Localized trigger aria-label | ✅ | ⚠️ | — | — | — | — |
| Controlled/uncontrolled + submit event | ✅ | ✅ | ✅ | ✅ | — | — |
⚠️ = partial (Mantine covers activation/submit subsets via activateOnFocus/submitOnBlur; AntD covers startWithEditMode-like behavior through the controlled editing flag; reka-ui hardcodes the trigger aria-label, while VeanUI localizes it with useLocaleMessages).
Cautions
- An external controlled update does not overwrite in-progress input: while editing,
modelValuepushes are not written into the edit buffer — the input is written back on submit/cancel, avoiding “typing being stomped”; outside edit mode the value stays synced. - Blur submission depends on focus leaving the component root:
useFocusOutsidelistens on the documentfocusinevent, sosubmitModelogic runs only when focus moves outside; withsubmitMode: 'none'blur just cancels. - Form integration: the
VisuallyHiddenInputproxy renders only when the component sits in a container with the.formclass (isFormControl) and anameis provided; otherwise no hidden input is emitted. placeholderaccepts a string or an{ edit, preview }object — separate texts for edit and preview states.readonlyanddisableddiffer:readonlyblocks entering edit (edit()guard + disabled triggers + readonly input) without touching the value;disabledadditionally disables preview focus and the hidden proxy.- A custom
previewslot has no defaulttabindex— provide a focusable element or activate viaEditableEditTrigger. - With
activationMode: 'none', focus does not enter edit — activate only via the edit trigger or the exposededit()(defineExpose). - In controlled usage, listen to
submitfor the final committed value (Enter/blur/submit trigger all follow the same path);update:modelValuefires at the same time.
FAQ
How do I edit on double-click instead of focus?
Set activationMode="dblclick":
<SEditable activation-mode="dblclick" default-value="Double click to edit" />How do I cancel on blur instead of submitting?
Set submitMode="none" — blur runs cancel() (no submit/update:modelValue), Esc still cancels, and Enter still submits:
<SEditable submit-mode="none" default-value="Blur cancels" />How do I trigger editing with a button?
Use EditableEditTrigger (available in the Aria composite), or call the exposed method via a ref:
<SEditable ref="editableRef" default-value="Edit via button" />
<button @click="editableRef?.edit()">Edit</button>How do I get the value after submitting?
Listen to the submit event (it fires together with update:modelValue):
<SEditable v-model="value" @submit="onSubmit" />function onSubmit(value: string) {
// the committed value
}How do I make the input resize to its content?
Enable autoResize — the input container uses an inline-grid layout and follows the content width:
<SEditable auto-resize default-value="Auto width" />How do I submit along with a native form?
Place the component inside a form container with the .form class and pass a name; the component renders a hidden input proxy that submits with the form:
<form class="form" @submit.prevent>
<SEditable name="nickname" default-value="Nickname" />
<button type="submit">Submit</button>
</form>How do I select all text when editing starts?
Set selectOnFocus — entering edit focuses the input and selects the existing content for direct overwrite:
<SEditable select-on-focus default-value="Select on focus" />