Editable
stimeo--editable
Editing in place: click or F2 to start, Enter to save, Esc to cancel.
Turns displayed text into something you can rewrite in place. Click or press F2 to start, and focus moves into the field with the text selected. Enter saves and Esc cancels; for longer text, Ctrl or Command with Enter saves. Either way focus returns to the display when you are done. Clicking elsewhere saves too, unless what you clicked is a button belonging to the editor, which means you have not left. Saving and cancelling are also available as buttons, so the keyboard is not required. You are told only when the value really changed. Whether it is being edited is readable from CSS, so the look is yours.
Keyboard
| Key | Action |
|---|---|
| Enter / Space / F2 | Enter edit mode from the display element. |
| Enter | Save (single-line); inserts a newline in a textarea. |
| Ctrl+Enter / Cmd+Enter | Save when editing a multiline textarea. |
| Esc | Discard changes and return to display mode. |
<%# Markup for the editable (inline editing) demo.
Click / Enter / Space / F2 on the display element (a button) enters edit mode, focusing
the input and selecting all. The library handles mode switching (data-mode), focus /
select-all, and the commit event; the look lives in demo.css.
Two variants are shown:
1. Single-line <input> with Save / Cancel buttons: Enter saves, Escape cancels, and
the buttons reach the same paths without a keyboard. Focus moving to a button
inside the editor does not commit, so Cancel really cancels.
2. Multiline <textarea>: Enter inserts a newline, Ctrl+Enter saves, Escape cancels.
submitOnBlur=true also saves on blur for both, when focus leaves the editor. %>
<div class="editable-demo">
<%# 1. Single-line inline edit. %>
<div class="editable" data-controller="stimeo--editable"
data-stimeo--editable-submit-on-blur-value="true">
<span class="editable__hint"><%= t("components.editable.demo.hint") %></span>
<button
type="button"
class="editable__display"
aria-label="<%= t("components.editable.demo.aria_label") %>"
data-placeholder="<%= t("components.editable.demo.placeholder") %>"
data-stimeo--editable-target="display"
data-action="click->stimeo--editable#edit keydown->stimeo--editable#onDisplayKeydown">
<%= t("components.editable.demo.value") %>
</button>
<input
type="text"
class="editable__input"
hidden
aria-label="<%= t("components.editable.demo.aria_label") %>"
data-stimeo--editable-target="input"
data-action="keydown->stimeo--editable#onKeydown" />
<%# Only meaningful while editing; demo.css hides the row in display mode. %>
<div class="editable__controls">
<button
type="button"
class="demo-trigger demo-trigger--primary"
data-action="click->stimeo--editable#save"><%= t(
"components.editable.demo.save"
) %></button>
<button
type="button"
class="demo-trigger"
data-action="click->stimeo--editable#cancel"><%= t(
"components.editable.demo.cancel"
) %></button>
</div>
</div>
<%# 2. Multiline inline edit. The controller detects the <textarea> and treats Enter as a
newline, so saving is Ctrl+Enter (or blur). The display uses white-space: pre-line so
saved line breaks render. %>
<div class="editable" data-controller="stimeo--editable"
data-stimeo--editable-submit-on-blur-value="true">
<span class="editable__hint"><%= t("components.editable.demo.multiline.hint") %></span>
<button
type="button"
class="editable__display editable__display--multiline"
aria-label="<%= t("components.editable.demo.multiline.aria_label") %>"
data-placeholder="<%= t("components.editable.demo.multiline.placeholder") %>"
data-stimeo--editable-target="display"
data-action="click->stimeo--editable#edit
keydown->stimeo--editable#onDisplayKeydown"><%= t(
"components.editable.demo.multiline.value"
) %></button>
<textarea
class="editable__input editable__input--multiline"
hidden
rows="3"
aria-label="<%= t("components.editable.demo.multiline.aria_label") %>"
data-stimeo--editable-target="input"
data-action="keydown->stimeo--editable#onKeydown"></textarea>
</div>
</div>
/*
* Presentation-only styles for the editable demo.
* The library switches modes via data-mode and each element's hidden attribute.
* Here we give the display element an "editable-looking" appearance and style the
* input while editing.
*/
.editable-demo {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.editable {
display: inline-flex;
flex-direction: column;
gap: 0.35rem;
/* Fixed width so switching display <-> edit does not resize the column: the display
button is content-sized but the <input>/<textarea> is not, which otherwise made the
multiline variant's layout jump when it entered edit mode. */
width: 18rem;
}
.editable__hint {
font-size: 0.8125rem;
color: var(--color-text-muted);
}
.editable__display {
text-align: left;
/* Saving an empty value leaves the button with no text. Without a floor the
click target would collapse to its padding, so the only way back into edit
mode would be a sliver of a strip. The floor matches the control it stands
in for, keeping the column the same height in both modes. */
min-height: 2.4rem;
padding: 0.4rem 0.6rem;
border: 1px dashed transparent;
border-radius: 0.375rem;
background: transparent;
color: var(--fg, var(--color-text));
font: inherit;
font-weight: 600;
cursor: text;
}
.editable__display:hover {
border-color: var(--border-strong);
background: var(--surface-subtle);
}
.editable__display:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Multiline display: render saved line breaks (textContent keeps the newlines). */
.editable__display--multiline {
white-space: pre-line;
/* Stands in for a three-row textarea, so entering edit mode does not jump. */
min-height: 5.4rem;
}
/* An emptied value still needs to say what it is and invite the next edit. The
wording is authored per locale and read from the attribute, so this rule stays
free of copy. */
.editable__display:empty::before {
content: attr(data-placeholder);
color: var(--color-text-muted);
font-weight: 400;
font-style: italic;
}
.editable__input {
width: 100%;
box-sizing: border-box;
padding: 0.4rem 0.6rem;
border: 1px solid var(--accent, var(--color-primary));
border-radius: 0.375rem;
font: inherit;
color: var(--fg, var(--color-text));
}
.editable__input--multiline {
resize: vertical;
}
/* Save / Cancel belong to edit mode only; data-mode is the library's hook for that. */
.editable__controls {
display: flex;
gap: 0.5rem;
}
.editable[data-mode="display"] .editable__controls {
display: none;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--editable"
Targets
| Name | Description | Attribute |
|---|---|---|
display
required
|
The element (typically a button) shown in display mode and activated to enter edit mode. | data-stimeo--editable-target="display" |
input
required
|
The input or textarea shown in edit mode that holds the editable value. | data-stimeo--editable-target="input" |
Values
| Name | Description | Attribute |
|---|---|---|
submitOnBlur
|
When true (default), losing focus while editing saves; when false, editing is kept. | data-stimeo--editable-submit-on-blur-value |
Actions
| Name | Description | Action |
|---|---|---|
cancel
|
Discards the edit, returns to display mode, and dispatches cancel. |
stimeo--editable#cancel |
edit
|
Enters edit mode: seeds the input from the declared value, focuses it, and selects its content. | stimeo--editable#edit |
onDisplayKeydown
|
Handles F2 on the display element as an extra entry point into edit mode. | stimeo--editable#onDisplayKeydown |
onKeydown
|
Commits on Enter (Ctrl/Cmd+Enter when multiline) and cancels on Escape while editing. | stimeo--editable#onKeydown |
revert
|
Puts back the value the last save replaced, once, without dispatching anything. | stimeo--editable#revert |
save
|
Commits the edit and returns focus to the display element. | stimeo--editable#save |
Events
| Name | Description | Event |
|---|---|---|
cancel
|
Dispatched when editing is cancelled via Escape, discarding the edits. | stimeo--editable:cancel |
change
|
Dispatched after a successful save only when the value changed, with { value, previous } in detail. |
stimeo--editable:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
data-mode |
Controller element | "display" or "editing". |
hidden |
Display / Input | Present on whichever element is not active for the current mode, re-derived when a target is swapped in at runtime. |
data-value |
Display | When authored, this holds the value; saving updates it and leaves the rendered text untouched. |