Editable
Editable is an input field used for editing a single line of text. It renders as static text and transforms into a text input field when then edit interaction is triggered (click, focus, or double-click).
Features
- Use custom controls for the editable.
- Pressing
Enter
commits the input value. - Pressing
Esc
reverts the value. - Activate edit mode by double-clicking or focusing on the preview text.
- Auto-resize input to fit content
Installation
To use the editable machine in your project, run the following command in your command line:
npm install @zag-js/editable @zag-js/react # or yarn add @zag-js/editable @zag-js/react
npm install @zag-js/editable @zag-js/vue # or yarn add @zag-js/editable @zag-js/vue
npm install @zag-js/editable @zag-js/vue # or yarn add @zag-js/editable @zag-js/vue
npm install @zag-js/editable @zag-js/solid # or yarn add @zag-js/editable @zag-js/solid
This command will install the framework agnostic editable logic and the reactive utilities for your framework of choice.
Anatomy
To set up the editable correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the editable package into your project
import * as editable from "@zag-js/editable"
The editable package exports two key functions:
machine
— The state machine logic for the editable widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the editable machine in your project 🔥
import * as editable from "@zag-js/editable" import { useMachine, normalizeProps } from "@zag-js/react" export default function Editable() { const [state, send] = useMachine(editable.machine({ id: "1" })) const api = editable.connect(state, send, normalizeProps) return ( <div {...api.getRootProps()}> <div {...api.getAreaProps()}> <input {...api.getInputProps()} /> <span {...api.getPreviewProps()} /> </div> </div> ) }
import * as editable from "@zag-js/editable" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, h, Fragment } from "vue" export default defineComponent({ name: "Editable", setup() { const [state, send] = useMachine(editable.machine({ id: "1" })) const apiRef = computed(() => editable.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.getRootProps()}> <div {...api.getAreaProps()}> <input {...api.getInputProps()} /> <span {...api.getPreviewProps()} /> </div> </div> ) } }, })
<script setup> import * as editable from "@zag-js/editable"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed } from "vue"; const [state, send] = useMachine(editable.machine({ id: "1" })); const api = computed(() => editable.connect(state.value, send, normalizeProps)); </script> <template> <div ref="ref" v-bind="api.getRootProps()"> <div v-bind="api.getAreaProps()"> <input v-bind="api.getInputProps()" /> <span v-bind="api.getPreviewProps()" /> </div> </div> </template>
import * as editable from "@zag-js/editable" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" export default function Editable() { const [state, send] = useMachine(editable.machine({ id: createUniqueId() })) const api = createMemo(() => editable.connect(state, send, normalizeProps)) return ( <div {...api().getRootProps()}> <div {...api().getAreaProps()}> <input {...api().getInputProps()} /> <span {...api().getPreviewProps()} /> </div> </div> ) }
Setting the initial value
To set the initial value of the editable, pass the value
property to the
machine's context.
const [state, send] = useMachine( editable.machine({ value: "Hello World", }), )
Listening for value changes
The editable machine supports two ways of listening for value changes:
onValueChange
: called when value changes.onValueCommit
: called when the value is committed.
const [state, send] = useMachine( editable.machine({ onValueChange(details) { console.log("Value changed", details.value) }, onValueCommit(details) { console.log("Value submitted", details.value) }, }), )
Using custom controls
In some cases, you might need to use custom controls to toggle the edit and read mode. We use the render prop pattern to provide access to the internal state of the component.
import * as editable from "@zag-js/editable" import { useMachine } from "@zag-js/react" export default function Editable() { const [state, send] = useMachine(editable.machine({ id: "1" })) const api = editable.connect(state, send) return ( <div {...api.getRootProps()}> <div {...api.getAreaProps()}> <input {...api.getInputProps()} /> <span {...api.getPreviewProps()} /> </div> <div> {!api.editing && <button {...api.getEditTriggerProps()}>Edit</button>} {api.editing && ( <div> <button {...api.getSubmitTriggerProps()}>Save</button> <button {...api.getCancelTriggerProps()}>Cancel</button> </div> )} </div> </div> ) }
import * as editable from "@zag-js/editable" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, h, Fragment } from "vue" export default defineComponent({ name: "Editable", setup() { const [state, send] = useMachine(editable.machine({ id: "1" })) const apiRef = computed(() => editable.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.getRootProps()}> <div {...api.getAreaProps()}> <input {...api.getInputProps()} /> <span {...api.getPreviewProps()} /> </div> <div> {!api.editing && <button {...api.getEditTriggerProps()}>Edit</button>} {api.editing && ( <div> <button {...api.getSubmitTriggerProps()}>Save</button> <button {...api.getCancelTriggerProps()}>Cancel</button> </div> )} </div> </div> ) } }, })
<script setup> import * as editable from "@zag-js/editable"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed } from "vue"; const [state, send] = useMachine(editable.machine({ id: "1" })); const api = computed(() => editable.connect(state.value, send, normalizeProps)); </script> <template> <div ref="ref" v-bind="api.getRootProps()"> <div v-bind="api.getAreaProps()"> <input v-bind="api.getInputProps()" /> <span v-bind="api.getPreviewProps()" /> </div> <div> <div v-if="api.editing"> <button v-bind="api.getSubmitTriggerProps()">Save</button> <button v-bind="api.getCancelTriggerProps()">Cancel</button> </div> <button v-else v-bind="api.getEditTriggerProps()">Edit</button> </div> </div> </template>
import * as editable from "@zag-js/editable" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, Show } from "solid-js" export default function Editable() { const [state, send] = useMachine(editable.machine({ id: createUniqueId() })) const api = createMemo(() => editable.connect(state, send, normalizeProps)) return ( <div {...api().getRootProps()}> <div {...api().getAreaProps()}> <input {...api().getInputProps()} /> <span {...api().getPreviewProps()} /> </div> <div> <Show when={!api().editing}> <button {...api().getEditTriggerProps()}>Edit</button>{" "} </Show> <Show when={api().editing}> <div> <button {...api().getSubmitTriggerProps()}>Save</button> <button {...api().getCancelTriggerProps()}>Cancel</button> </div> </Show> </div> </div> ) }
Auto-resizing the editable
To auto-grow the editable as the content changes, pass the autoResize: true
property to the machine's context.
const [state, send] = useMachine( editable.machine({ autoResize: true, }), )
Please note that, in this mode, the input and preview elements should not have any styles. Instead, pass the styles to the "area" element.
Setting a maxWidth
It is a common pattern to set a maximum of the editable as it auto-grows. To
achieve this, set the maxWidth
property of the machine's context to the
desired value.
const [state, send] = useMachine( editable.machine({ autoResize: true, maxWidth: "320px", }), )
When the editable reaches the specified max-width, it'll clip the preview text with an ellipsis.
Editing with double click
The editable supports two modes of activating the "edit" state:
- when the preview part is focused (with pointer or keyboard).
- when the preview part is double-clicked.
To change the mode to "double-click", set the activationMode: 'dblclick'
property in the machine's context.
const [state, send] = useMachine( editable.machine({ activationMode: "dblclick", }), )
Usage with Textarea
The editable machine supports using a textarea
instead of an input
field.
When a textarea is used, the editable will commit the value on Cmd + Enter
or
Ctrl + Enter
.
Use the
api.inputProps
to spread the input props to the textarea element. You might need to cast the input props to the correct type.
<textarea {...(api.inputProps as HTMLTextareaProps<HTMLTextareaElement>)} />
Styling guide
Earlier, we mentioned that each editable part has a data-part
attribute added
to them to select and style them in the DOM.
Focused state
When the editable is in the focused mode, we set a data-focus
attribute on the
"area" part.
[data-part="area"][data-focus] { /* CSS for the editable's focus state */ }
Empty state
When the editable's value is empty, we set a data-empty
attribute on the
"area" part.
[data-part="area"][data-empty] { /* CSS for the editable's focus state */ }
Disabled state
When the editable is disabled, we set a data-disabled
attribute on the "area"
part.
[data-part="area"][data-disabled] { /* CSS for the editable's focus state */ }
Methods and Properties
Machine Context
The editable machine exposes the following context properties:
ids
Partial<{ root: string; area: string; label: string; preview: string; input: string; controls: string; submitTrigger: string; cancelTrigger: string; editTrigger: string; }>
The ids of the elements in the editable. Useful for composition.invalid
boolean
Whether the input's value is invalid.name
string
The name attribute of the editable component. Used for form submission.form
string
The associate form of the underlying input.autoResize
boolean
Whether the editable should auto-resize to fit the content.activationMode
ActivationMode
The activation mode for the preview element. - "focus" - Enter edit mode when the preview element is focused - "dblclick" - Enter edit mode when the preview element is double-clicked - "none" - No interaction with the preview element will trigger edit mode.submitMode
SubmitMode
The action that triggers submit in the edit mode: - "enter" - Trigger submit when the enter key is pressed - "blur" - Trigger submit when the editable is blurred - "none" - No action will trigger submit. You need to use the submit button - "both" - Pressing `Enter` and blurring the input will trigger submitstartWithEditView
boolean
Whether to start with the edit mode active.selectOnFocus
boolean
Whether to select the text in the input when it is focused.value
string
The value of the editable in both edit and preview modemaxLength
number
The maximum number of characters allowed in the editabledisabled
boolean
Whether the editable is disabledreadOnly
boolean
Whether the editable is readonlyonValueChange
(details: ValueChangeDetails) => void
The callback that is called when the editable's value is changedonValueRevert
(details: ValueChangeDetails) => void
The callback that is called when the esc key is pressed or the cancel button is clickedonValueCommit
(details: ValueChangeDetails) => void
The callback that is called when the editable's value is submitted.onEdit
() => void
The callback that is called when in the edit mode.placeholder
string | { edit: string; preview: string; }
The placeholder value to show when the `value` is emptytranslations
IntlTranslations
Specifies the localized strings that identifies the accessibility elements and their statesfinalFocusEl
() => HTMLElement
The element that should receive focus when the editable is closed. By default, it will focus on the trigger element.dir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => Node | ShadowRoot | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the componentonFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the componentonInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the component
Machine API
The editable api
exposes the following methods:
editing
boolean
Whether the editable is in edit modeempty
boolean
Whether the editable value is emptyvalue
string
The current value of the editablevalueText
string
The current value of the editable, or the placeholder if the value is emptysetValue
(value: string) => void
Function to set the value of the editableclearValue
() => void
Function to clear the value of the editableedit
() => void
Function to enter edit modecancel
() => void
Function to exit edit mode, and discard any changessubmit
() => void
Function to exit edit mode, and submit any changes
Data Attributes
Accessibility
Keyboard Interactions
- EnterSaves the edited content and exits edit mode.
- EscapeDiscards the changes and exits edit mode.
Edit this page on GitHub