Editable
Show a value as text and edit it in place in its field.
Installation
pnpm dlx shadcn@4.18.0 add @siteplane/editableComplete the one-time setup first. Registry JSON lists source files and dependencies.
For this page’s examples, also install:
pnpm dlx shadcn@4.18.0 add @siteplane/cardField is installed with Input, which Editable installs.
Usage
import {
Editable,
EditableArea,
EditableControl,
EditableEditTrigger,
EditableInput,
EditablePreview,
} from "@/siteplane/components/editable";
<Editable defaultValue="Spring campaign" labels={{ edit: "Rename project" }}>
<EditableArea>
<EditablePreview />
<EditableInput aria-label="Project name" />
</EditableArea>
<EditableControl>
<EditableEditTrigger />
</EditableControl>
</Editable>;Composition
Editable
├── EditableArea
│ ├── EditablePreview
│ └── EditableInput or EditableTextarea
└── EditableControl
└── EditableEditTriggerAt rest, EditablePreview shows the value and EditableEditTrigger shows a pencil beside it. The pencil opens the field in the same place, filled with the value and with its text selected. Enter, or a click or a focus move outside the editable, keeps the change and calls onValueCommit. Escape puts back the value from when editing started and calls onValueRevert. Focus then returns to the pencil. The field stays in the document while it is hidden, so its name submits with a form.
The text never moves: the value stands where the field's text stands, and only the field's frame fades in around it when the field opens and out when it closes. At rest the row is as tall as the field and the value starts where the field's text starts, so the frame stands where every field stands, flush with its label and 8 px under it. Focus moves into the field at once, so typing right after the pencil loses no keystroke. With reduced motion, the frame appears and goes at once.
Label the value with a Field and FieldLabel, as in the example above, or give EditableInput an aria-label. Give every Editable a placeholder, such as “Add a project name”, so that an empty value still reads. Name the pencil after the value with labels.edit, such as “Rename project”, so that several pencils on one page can be told apart.
Editable suits a value that people read far more often than they change, such as the title of a project or a page, or the name of an item in a list. A settings form keeps its fields open instead, in a Form with one Save for each section, so nothing has to be switched into a field first.
Names in a list
In a list, each name is an Editable of its own. Name each pencil after its item, such as “Rename Open deals”, and save the new name in onValueCommit. With activationMode="none" only the pencil opens the field, so the keyboard reaches one stop per item.
- Open deals
- Renewals this quarter
- My tasks
While editing, the root carries data-editing. Give it a width there when its container sizes to its content, such as data-editing:w-80, so the field has room for a longer name.
Validation
Inside a Field, Enter and leaving check the value before they keep it: the native constraints of the field, such as required or type="email", and the Field’s validate. A value that fails stays in its field, and FieldError shows the message under it. Without children, FieldError shows the browser’s message for a native constraint. Escape puts back the saved value and clears the error.
Issue IDs start with it, as in ENG-128.
Without a Field, the browser shows its own message for a native constraint, and the value stays in its field until it passes.
Multi-line value
EditableTextarea edits text with line breaks. Enter starts a new line, and Cmd+Enter on Apple systems or Ctrl+Enter elsewhere saves. At rest the preview wraps the text and keeps its line breaks, inside the textarea's insets. The textarea keeps the pencil's room at its end, so it wraps the text in the same width and every line breaks where it broke at rest.
Controlled
value with onValueChange puts the value in your application, and edit with onEditChange the edit state. onValueChange receives every change, also while typing, and the value that a cancel puts back; keep the saved value from onValueCommit. A controlled edit state only asks: the pencil, Enter, Escape and leaving call onEditChange, and the edit starts or ends once edit changes. finalFocusEl returns focus to another element afterwards, such as the button that started editing.
The callbacks receive the value itself as their first argument, not an object.
Saving and leaving
submitMode sets what saves a change: both, the default, saves on Enter and when focus or a press leaves the editable; enter saves on Enter alone; blur on leaving alone; and none only with an EditableSubmitTrigger. Where leaving does not save, it cancels. Leaving means a press or a focus move outside the editable and its Field, so the label, the hint and the error belong to it. Focus that moved to another element stays there. Inside a Field, leaving with a value that fails its check keeps the field open with the error.
activationMode sets what opens the field from the preview: focus, the default, opens it when the preview receives focus; click on a click; dblclick on a double click; and none only from the pencil. The preview takes focus only while it can open the field.
Keyboard
| Key | Action |
|---|---|
| Tab | At rest, moves to the preview and the pencil. With the default activationMode, focus on the preview opens the field. |
| Enter or Space | On the pencil, opens the field with its text selected. |
| Enter | In a single-line field, saves when submitMode is both or enter. It never submits a form around the editable. |
| Cmd+Enter or Ctrl+Enter | In a textarea, saves: Cmd on Apple systems, Ctrl elsewhere. Enter alone starts a new line. |
| Escape | Puts the value back and closes the field. A dialog around the editable stays open. |
| Tab or Shift+Tab out of the editable | Saves when submitMode is both or blur, and cancels otherwise. Focus stays where it went. |
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
Editable. | string | unset | The value, for a controlled Editable. It holds the change being typed; keep the saved value from onValueCommit. |
Editable. | string | "" | The value an uncontrolled Editable starts with. |
Editable. | (value: string) => void | unset | Called with every change, also while typing, and with the value a cancel puts back. |
Editable. | (value: string) => void | unset | Called with the value when a change is saved. |
Editable. | (value: string) => void | unset | Called with the value a cancel puts back. |
Editable. | boolean | unset | Whether the field shows, for a controlled edit state. |
Editable. | boolean | false | Whether an uncontrolled Editable starts with its field open and focused. |
Editable. | (edit: boolean) => void | unset | Called when editing should start (true) or end (false). |
Editable. | focus | click | dblclick | none | focus | What opens the field from the preview. With none, only the pencil opens it, and the preview takes no focus. |
Editable. | both | enter | blur | none | both | What saves a change: Enter, leaving the editable, both or only Save. Leaving an editable that does not save cancels the change. |
Editable. | boolean | true | Selects the text of the field when it opens. |
Editable. | string | { edit: string; preview: string } | unset | The placeholder of the field, and the text of the preview while the value is empty; an object sets them apart. |
Editable. | boolean | false | Turns off the preview, the field and the triggers. A disabled Field or Fieldset around it does the same. |
Editable. | boolean | false | Shows the value with a disabled pencil, so it cannot be edited. |
Editable. | boolean | false | required marks the field required, which Save and Enter check. invalid marks the field and the root invalid, as an error from your server does. |
Editable. | string / string / number | unset | Native props of the field: the name it submits with, the id of a form outside it, and the most characters. |
Editable. | () => HTMLElement | null | the pencil | The element that takes focus when an edit ends. Without a pencil, focus returns to the preview. |
Editable. | sm | md | lg | xl | provider size | The control size of the field, and of the optional Cancel and Save. The pencil stays icon-sm. |
Editable. | Partial<EditableLabels> | English | Replaces the words: edit (Edit, the name of the pencil), submit (Save) and cancel (Cancel). |
Editable | div props | — | Holds the preview and the field. While editing it takes the row. |
Editable | span props | — | The value at rest, where the field's text stands: on one line that ends with an ellipsis, or wrapped inside a textarea's insets. Children replace the text it shows, such as a formatted value. |
Editable | Input props without value and defaultValue | — | The single-line field, the kit Input. Props such as type, autoComplete or aria-label pass through. |
Editable | Textarea props without value and defaultValue | — | The multi-line field, the kit Textarea, with minLines and maxLines. |
Editable | div props | — | Holds the pencil, 8 px from the value, and the optional Cancel and Save. It lays out no box of its own. |
Editable | Button props | variant="ghost" size="icon-sm" | The pencil, named by labels.edit or aria-label. Children replace the pencil and then name the button. |
Editable | Button props | variant="ghost" / variant="accent" | Optional Cancel and Save Buttons beside the field, shown while editing; Enter, leaving and Escape do without them. They say labels.cancel and labels.submit, and children replace the word. submitMode none saves only with EditableSubmitTrigger. |
EditableProps extends native div props without defaultValue, onChange and
placeholder. Every part accepts a ref and a string className. The root carries
data-editing while editing, as well as data-disabled, data-readonly and
data-invalid; EditableArea and EditablePreview carry
data-placeholder-shown while the value is empty. The triggers are buttons of
type button, so they never submit a form. The types EditableLabels,
EditablePlaceholder, EditableActivationMode and EditableSubmitMode are exported.
Usage notes
Use Editable for a single value shown as text that people rename now and then, such as a project or page title or the name of a list item. Settings forms keep their fields open: compose Input or Textarea in a Field, inside a Form with one Save for each section. A value with more than one part, such as an address, is edited in a Dialog with a form.
Show what the change does near the value, for example with a Toast after onValueCommit saved it, and keep the request and its error state in your application. An error from your server can come back through invalid or the errors of a Form.
Editable has some limits. The field takes the width of the row and does not grow with its text. A click on the FieldLabel does not open the field. There are no props for the ids of the parts, no provider or hook that reads the edit state from outside, and no callbacks for a press or a focus move outside the editable.