Inline Edit
Click-to-edit text that renders as plain content until activated, then swaps to a pre-filled field. Enter or blur commits, Escape cancels. Built for track renames, titles, and other fields where a permanent input box would be visual noise.
<arc-inline-edit>Overview
Inline Edit is editable text. In its resting state it renders the current value as plain text with no field chrome at all, only a faint pencil affordance on hover or focus. Clicking it, or pressing Enter or F2 while it is focused, swaps in a field pre-filled with the current value, text selected and ready to overtype. Enter or clicking away commits; Escape throws the edit away.
The display text inherits the surrounding typography, and the edit field matches it, so the swap never changes the text's size or position. An Inline Edit inside a heading edits at heading size; one in a table cell edits at cell size. This is the component's whole reason to exist: rename flows and title fields where a visible input box would say "form" when the page is saying "document".
While the user types, keystrokes accumulate in an internal draft and stream out as arc-input events. The value prop and the value a surrounding form submits change only when the edit commits, which fires a single arc-change. Committing an unchanged value fires nothing, so listeners never see a rename that didn't happen. Cancelling fires arc-cancel and restores the previous text.
The same three transitions are available from script. edit() enters edit mode with the field focused and its text selected. Use it for a "Rename" item in a context menu, or to focus a freshly added row so the user can name it without hunting for it. commit() and cancel() leave edit mode the two ways the keyboard does, firing the same arc-change (or nothing, if the value is unchanged) and arc-cancel. Reach for commit() when something outside the component ends the edit (a toolbar Save, a route change), and cancel() when a save fails and the previous text should come back. edit() is a no-op when the component is disabled, readonly, or already editing, so it is safe to call without checking first.
Inline Edit participates in forms through the same ElementInternals machinery as Input: give it a name and the committed value is submitted, required makes an empty committed value invalid (shown as a quiet error tint even in display state), and form.reset() restores the initial text. The multiline prop swaps the edit field to a textarea, where Enter inserts a newline and Cmd/Ctrl+Enter commits.
Guidelines
When to use
- Use Inline Edit where the text is content first and a field second: titles, track names, table cells, sidebar labels
- Always provide a `label`. It becomes the accessible name ("Edit Track title") for the display button and the field
- Listen for `arc-change` to persist a rename; it fires once per commit and only when the value actually changed
- Use `multiline` for short notes and descriptions that may wrap, and tell users that Cmd/Ctrl+Enter saves
- Set a domain-specific `placeholder` ("Untitled track") so an empty value still reads as something clickable
- Use `readonly` when a value is temporarily locked. The text stays in the reading order without inviting an edit
When not to use
- Do not use Inline Edit in a conventional form layout. A labeled Input communicates "fill me in"; Inline Edit hides that invitation
- Do not use it for values needing heavy validation or structured entry (emails, dates, numbers). Use Input, DatePicker, or NumberInput
- Do not treat `arc-input` as a save signal; it carries the in-progress draft, which Escape may still throw away
- Do not hide the only editing path behind hover alone on touch-heavy interfaces. The affordance also appears on focus, so keep the control reachable by keyboard
Features
- Renders as plain text until activated. No field chrome in the resting state
- Display text inherits surrounding typography, and the edit field matches it, so the swap never reflows
- Activation by click, or Enter, Space, or F2 while focused; the field opens pre-filled with the text selected
- Enter or blur commits and fires a single `arc-change`; Escape reverts and fires `arc-cancel`
- Committing an unchanged value fires no event at all
- `arc-input` streams the draft on every keystroke while editing
- Multiline mode edits in a textarea: Enter inserts a newline, Cmd/Ctrl+Enter commits
- Full form participation: named submission of the committed value, `required` validation, reset support
- Programmatic control through `edit()`, `commit()`, and `cancel()`. The same three transitions the keyboard drives, firing the same events
- Pencil affordance and hover tint follow the design tokens; the swap animates subtly and honors reduced motion
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available. Use the Web Component directly or a framework wrapper.
<!-- A renameable track list: text until clicked, field until committed -->
<div style="display:flex; flex-direction:column; max-width:440px; gap:2px;">
<arc-inline-edit value="Midnight Signal" label="Track 1 title" placeholder="Untitled track"></arc-inline-edit>
<arc-inline-edit value="Glass Harbor" label="Track 2 title" placeholder="Untitled track"></arc-inline-edit>
<arc-inline-edit label="Track 3 title" placeholder="Untitled track"></arc-inline-edit>
</div>
<script>
document.querySelectorAll('arc-inline-edit').forEach((el) => {
el.addEventListener('arc-change', (e) => {
console.log('renamed to', e.detail.value);
});
});
</script>
<!-- Multiline notes: Enter newlines, Cmd/Ctrl+Enter commits -->
<arc-inline-edit multiline label="Session notes" placeholder="Add a note"></arc-inline-edit>import { InlineEdit } from '@arclux/arc-ui-react';
export default function TrackList() {
return (
<div style={{ display: 'flex', flexDirection: 'column', maxWidth: 440, gap: 2 }}>
<InlineEdit
value="Midnight Signal"
label="Track 1 title"
placeholder="Untitled track"
onArcChange={(e) => console.log('renamed to', e.detail.value)}
/>
<InlineEdit value="Glass Harbor" label="Track 2 title" placeholder="Untitled track" />
<InlineEdit label="Track 3 title" placeholder="Untitled track" />
</div>
);
}<script setup>
import { InlineEdit } from '@arclux/arc-ui-vue';
function onRename(e) {
console.log('renamed to', e.detail.value);
}
</script>
<template>
<div style="display:flex; flex-direction:column; max-width:440px; gap:2px;">
<InlineEdit value="Midnight Signal" label="Track 1 title" placeholder="Untitled track" @arc-change="onRename" />
<InlineEdit value="Glass Harbor" label="Track 2 title" placeholder="Untitled track" />
<InlineEdit label="Track 3 title" placeholder="Untitled track" />
</div>
</template><script>
import { InlineEdit } from '@arclux/arc-ui-svelte';
function onRename(e) {
console.log('renamed to', e.detail.value);
}
</script>
<div style="display:flex; flex-direction:column; max-width:440px; gap:2px;">
<InlineEdit value="Midnight Signal" label="Track 1 title" placeholder="Untitled track" on:arc-change={onRename} />
<InlineEdit value="Glass Harbor" label="Track 2 title" placeholder="Untitled track" />
<InlineEdit label="Track 3 title" placeholder="Untitled track" />
</div>import { Component } from '@angular/core';
import { InlineEdit } from '@arclux/arc-ui-angular';
@Component({
imports: [InlineEdit],
template: `
<div style="display:flex; flex-direction:column; max-width:440px; gap:2px;">
<arc-inline-edit value="Midnight Signal" label="Track 1 title" placeholder="Untitled track" (arc-change)="onRename($event)"></arc-inline-edit>
<arc-inline-edit value="Glass Harbor" label="Track 2 title" placeholder="Untitled track"></arc-inline-edit>
<arc-inline-edit label="Track 3 title" placeholder="Untitled track"></arc-inline-edit>
</div>
`,
})
export class TrackListComponent {
onRename(e: CustomEvent<{ value: string }>) {
console.log('renamed to', e.detail.value);
}
}import { InlineEdit } from '@arclux/arc-ui-solid';
export default function TrackList() {
return (
<div style={{ display: 'flex', 'flex-direction': 'column', 'max-width': '440px', gap: '2px' }}>
<InlineEdit
value="Midnight Signal"
label="Track 1 title"
placeholder="Untitled track"
on:arc-change={(e) => console.log('renamed to', e.detail.value)}
/>
<InlineEdit value="Glass Harbor" label="Track 2 title" placeholder="Untitled track" />
<InlineEdit label="Track 3 title" placeholder="Untitled track" />
</div>
);
}import { InlineEdit } from '@arclux/arc-ui-preact';
export default function TrackList() {
return (
<div style={{ display: 'flex', flexDirection: 'column', maxWidth: 440, gap: 2 }}>
<InlineEdit
value="Midnight Signal"
label="Track 1 title"
placeholder="Untitled track"
onArcChange={(e) => console.log('renamed to', e.detail.value)}
/>
<InlineEdit value="Glass Harbor" label="Track 2 title" placeholder="Untitled track" />
<InlineEdit label="Track 3 title" placeholder="Untitled track" />
</div>
);
}<div style="display:flex; flex-direction:column; max-width:440px; gap:2px;">
<arc-inline-edit value="Midnight Signal" label="Track 1 title" placeholder="Untitled track"></arc-inline-edit>
<arc-inline-edit value="Glass Harbor" label="Track 2 title" placeholder="Untitled track"></arc-inline-edit>
<arc-inline-edit label="Track 3 title" placeholder="Untitled track"></arc-inline-edit>
</div>API
valuestring''- The committed text. Updated only when an edit commits (Enter or blur); keystrokes accumulate in an internal draft until then.
labelstring''- Accessible name for the control. The display state announces as "Edit {label}" and the edit field is labeled with it. Always provide one.
namestring''- The
nameattribute sent with form data on submission. The submitted value is the committedvalue, never an in-progress draft. placeholderstring'Empty'- Text shown in muted italic when
valueis empty, and as the field placeholder while editing. Defaults to "Empty". disabledbooleanfalse- Prevents activation and applies a muted treatment. The value is excluded from form submission while disabled.
multilinebooleanfalse- When true, editing uses a
<textarea>: Enter inserts a newline and Cmd/Ctrl+Enter commits. Single-line commits on plain Enter. requiredbooleanfalse- Marks the field as required. An empty committed value is invalid, including in display state, which shows a subtle error tint.
readonlybooleanfalse- Renders the display state only: the text remains focusable for reading order, but activation is inert and no pencil affordance appears.
formAssociatedbooleantruepropertiesobject{ // flag(), unlike `disabled`. The exclusion in props.js is specifically // about form-associated *platform* semantics: a `disabled` content // attribute that is merely present makes the element actually disabled // per the HTML spec, and formDisabledCallback assigns the property back, // so no converter can win. Neither of these is platform-mapped: // `required` is enforced by _computeValidity() below and `readonly` by // each component's own interaction handlers, so the stock converter buys // nothing here and costs the usual bug: `required="false"` read as true, // blocking submission of a form the author meant to leave optional. // Finding #48's shape, across all 26 form controls at once. required: flag(false), readonly: flag(false), }- Lit merges static properties up the prototype chain, so every consumer
gets these without declaring them.
requiredparticipates in constraint validation below;readonlyreflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state). autoValidatesbooleantrue- Components that run their own constraint-validation logic (pattern checks, range checks) opt out of the automatic required sync by overriding this to false, and own the whole validity flag set instead.
formvalidityvalidationMessage
Methods
focus(options?)options?: FocusOptions- Move focus to whichever element is currently the control: the field while
editing, the display row otherwise.
The host sets no
delegatesFocusand everything focusable lives in the shadow root, soel.focus()used to do nothing at all, silently, which cost a consumer a "Rename" menu item that appeared dead.edit()is the way *into* editing and stays that way; this is only the obvious call landing where a caller expects it. edit()- Enter edit mode: focus the field and select its text. No-op when disabled, readonly, or already editing.
commit()- Commit the current draft and leave edit mode. Fires arc-change only if the value changed.
cancel()- Discard the draft, keep the previous value, and leave edit mode. Fires arc-cancel.
checkValidity()boolean- Whether the control currently satisfies its constraints, per the native
constraint-validation API. Fires
invalidon the element when it does not, and reports nothing to the user. reportValidity()boolean- As checkValidity(), but also shows the browser's validation message against the control when it fails.
Events
arc-changedetail:{ value: string }- Fired once on commit when the value actually changed, with the new value in detail.value.
arc-canceldetail:{ value: string }- Fired when an edit is canceled (Escape or cancel()), with the retained value in detail.value. No arc-change accompanies it.
arc-inputdetail:{ value: string }- Fired on each keystroke while editing, with the draft text in detail.value.
See Also
- InputVersatile form control supporting single-line text, email, password, and multiline textarea modes with built-in label, placeholder, and validation states. Pairs with Form for complete data-entry workflows.
- TextareaMulti-line text input with integrated label, placeholder, resize control, and live character count that turns red at the limit.
- FormForm wrapper with built-in validation, error aggregation, and submit handling. Composes Input, Textarea, and Button into a cohesive data-entry workflow.
- LabelForm label with required indicator, optional description text, and tooltip slot. Pairs with any input component via the `for` attribute.