4 InlineEdit
L4stIdi0t edited this page 2026-10-09 22:01:06 +02:00

InlineEdit

A value shown as plain text that becomes a field when you click it — for a figure that is read far more often than it is set: a dimension in a readout, a layer's name.

A permanent input there is noise, a dialog is too heavy. The contract:

  • At rest it is a real <button> whose accessible name carries both the value and the action ("39 mm, edit"). Readonly, it is plain text; disabled, a native disabled button — dimmed, out of the tab order, and exempt from contrast rules the way every disabled control in the kit is. Neither one opens.
  • Editing swaps in the kit's Input (type="text") or NumberInput (type="number") in place, focused with its contents selected.
  • Enter and blur commit, Escape cancels. Enter and Escape hand focus back to the button so a keyboard user is not dropped on <body>; blur leaves focus wherever the user sent it.
  • An arrow step inside NumberInput is a commit to NumberInput, not to this: only Enter, blur and Escape leave edit mode, and onValueCommit fires once, and only when the value actually changed.

Motion: the text never moves. The field is sized to its contents and pulled out by its own padding, so its text lands exactly where the resting text was; opening only fades the field's border, fill and ring in around it. Closing leaves a ghost of that border shrinking onto the text — tinted when the commit changed the value. Both snap under reduced motion.

Forms · two-way: bind:value bind:editing · keyboard contract

<script lang="ts">
	import { InlineEdit } from '@l4st/driftwood';
</script>
Light Dark
inline-edit inline-edit in dark mode

Props

Prop Type Default What it does
value bind: T — The value. Written back only on a commit that changed it.
type 'text' | 'number' 'text' Which field editing opens: Input or NumberInput.
min number — Lower bound, for type="number". Clamped on commit by NumberInput.
max number — Upper bound, for type="number".
step number — Arrow-key and rounding increment, for type="number".
suffix string — Unit shown after the value, and inside the NumberInput, e.g. 'mm'.
placeholder string — Shown in place of the value while it is empty, and in the empty field.
format (value: T) => string — How the resting text reads. Defaults to the value, a space, and the suffix.
editing bind: boolean false Whether the field is open. Set it to open the editor from outside.
disabled boolean false Shown dimmed, out of the tab order; never opens.
readonly boolean false Shown as text; never opens.
id string — Goes on the button at rest and on the field while editing.
aria-label string — What the value is, e.g. 'Height'. Prefixed to the button's name and names the field.
editLabel string 'edit' The action word the button's accessible name ends with.
class string — Merged with the component's own classes, last — so yours win.
inputClass string — Merged onto the field while editing. By default it hugs its text; give it a width here to fix one.
onValueCommit (value: T) => void — Enter or blur, with a value different from the one editing started with.
onCancel () => void — Escape: the edit was abandoned and the value is unchanged.

Keyboard

Key Does
Enter / Space open the field, from the text
Enter commit, and return focus to the text
Escape cancel, restore the value and return focus to the text
Tab commit by leaving — focus goes where Tab sends it
↑ / ↓ step, when it is a number — the field stays open

Example

The markup behind the screenshot above, lifted from src/routes/shots/ — so it compiles, and it is exactly what produced the image.

<div class="flex items-center justify-between gap-4">
	<span class="text-muted-foreground">Layer</span>
	<InlineEdit bind:value={layerName} aria-label="Layer name" />
</div>
<div class="flex items-center justify-between gap-4">
	<span class="text-muted-foreground">Height</span>
	<InlineEdit type="number" bind:value={binHeight} min={7} max={400} suffix="mm" aria-label="Height" />
</div>
<div class="flex items-center justify-between gap-4">
	<span class="text-muted-foreground">Depth</span>
	<InlineEdit type="number" value={42} suffix="mm" aria-label="Depth" editing />
</div>

Notes

  • Reach for it when the value is read far more than it is set. A readout of dimensions, a name in a list header: a permanent field there is noise around every figure. If the value changes often, or sits in a form next to other fields, use a plain Input or NumberInput instead: a form should look like a form.
  • value only changes on a commit. Typing goes into a draft, so a binding never sees a half-typed "1" on the way to "120", and Escape has nothing to undo. onValueCommit fires once per edit, and not at all when the result equals what editing started with.
  • Arrows do not close it. NumberInput treats an arrow step as a commit of its own. Here it only moves the draft, so a user can step three times and then press Enter. The draft is clamped and snapped by NumberInput, so what lands in value is always within min/max and on the step grid.
  • Name it with aria-label. The button's accessible name is built from it, as "Height, 39 mm, edit", and the field gets it as its own name while it is open. editLabel is the word at the end, so it can be translated.
  • bind:editing opens it from outside, for a "Rename" item in a menu, say. The field takes focus the same way it does on a click. The generic T follows value, so onValueCommit receives a number when you bind a number.
  • inputClass sizes the open field. The defaults (w-40 for text, w-28 for a number, both h-8) sit in a line of text-sm without moving it much. A wider value wants a wider field.

Forms · Field · Input · FileInput · FileList · Textarea · CodeEditor · Combobox · RadioGroup · NumberInput · Label · Select · MultiSelect · DatePicker · TagsInput · Checkbox · Switch · Slider


← All components · Getting started · Theming · source