Skip to main content
WireKit
Copy for LLM

Inline Edit

Turns a displayed value into an editor in place and back again. The reader stays where they are — no dialog, no separate form page — and nothing is written until they confirm.

Not for a yes/no value. Reach for toggle there: the control is already its own read state, so the confirm/cancel machinery buys nothing and costs two extra tab stops per row. This component earns its weight when the value is text a reader has to type — and only then.

Basic Usage

Edit a title in place

The component does not save anything. It emits an event when the reader confirms, and your Livewire component decides what that means:

<x-wirekit::inline-edit
    name="title"
    label="Project title"
    :value="$post->title"
    :context="$post->reference"
    wire:wirekit:inline-edit-confirmed="save($event.detail.value)" />
public function save(string $value): void
{
    $this->post->update(['title' => $value]);

    // Tell the component it worked. It does NOT infer this from the round trip
    // ending — a validation failure ends one too.
    $this->dispatch('wirekit:inline-edit-saved', name: 'title');
}

Livewire compiles any non-reserved wire:<event> into an Alpine listener, so no wrapping element is needed.

The editor stays open until the save is confirmed

This is the part worth reading twice. When the reader confirms, the component enters a saving state and waits. It closes when you say the save succeeded, and not before.

That is deliberate: the end of a round trip is not evidence of success. A validation failure completes a request too, and a component that closed there would discard the error message you just rendered, leaving the reader looking at their old value with no explanation.

Three ways the outcome is reported:

Event Meaning
wirekit:inline-edit-saved Written. The editor closes and focus returns to the trigger
wirekit:inline-edit-failed Rejected. The editor stays open, focus does not move, so the reader is still looking at what they must fix
neither, within saveTimeout No answer arrived. The editor gives up rather than staying busy forever, and says the state is unknown

Filter by name in the event detail when a page holds several editors.

Livewire

The component owns the interaction; your component owns the write. It emits wirekit:inline-edit-confirmed with name, value and previous, and waits.

wire:model.blur does not save in Livewire 4. .live became its own modifier, so what used to mean "send on blur" is now spelled wire:model.live.blur. Every older guide — and muscle memory — says otherwise, and the failure is silent: the field looks bound and nothing reaches the server. Do not reach for it here in any case; this component commits explicitly, which is the point of it.

Do not bind the draft two-way. A wire:model on the control writes on every keystroke, which leaves nothing to confirm and nothing for cancel to restore. The draft stays local until the reader commits — that is the whole design, not an optimization.

Validation errors

A validation failure comes back as re-rendered HTML with a filled error bag, not as a transport error. The component reads the bag under the field's name, keeps the editor open, links the message with aria-describedby, and announces it:

public function save(string $value): void
{
    $this->validate(['title' => 'required|max:120'], [], ['title' => $value]);
    // On failure Livewire re-renders and the bag is filled — nothing else to do.

    $this->post->update(['title' => $value]);
    $this->dispatch('wirekit:inline-edit-saved', name: 'title');
}

Surviving a morph

A Livewire morph must not close an open editor or lose a draft. Three rules make that hold, and all three are in the component already:

  • The mode switch uses x-show, not x-if. Livewire stops patching attributes while a visibility state differs, so x-show survives; x-if would tear the open editor down mid-edit.
  • Nothing server-rendered sits on the element that toggles. An id or an aria-busy there would stop being updated by the morph.
  • There is no wire:ignore on the root — it would cut the error text and the displayed value off from updates too.

Add wire:key when you render these in a loop, so a morph keeps each editor attached to its own row.

Several editors at once

exclusive (default true) closes any other open editor when one opens. Without it a reader can carry ten unsaved drafts, and a wire:navigate takes all ten with it. If you turn it off, the browser still warns before a navigation that would discard an open draft.

Affordance

Two independent choices: what the reader sees, and what opens the editor.

The three trigger styles

always costs a tab stop per field. Twenty editable rows are twenty extra stops before anyone reaches the content below them. In a dense table hover or focus-only is usually the kinder choice — but pick it for that reason, not because the pencil looks untidy.

There is deliberately no way to remove the button. Plain text is not focusable, so a value with no button cannot be reached by keyboard at all. focus-only gives the quiet appearance without that defect: nothing is shown until the reader tabs to it, and then it is a real, visible, full-size button.

openOnValueClick (default true) makes the value itself clickable. It is a separate switch because it answers a different question, and the value never becomes the button — a button named Jane Doe tells a screen-reader user nothing about what it does, and giving a control a name that is not its visible text breaks voice control.

Two behaviors follow from that clickable value, and both only show up on a real page:

  • Dragging across the value selects text, it does not open the editor. Selecting requires moving the mouse, so a movement beyond a few pixels is read as a selection.
  • A link inside the value follows the link. Otherwise the link would be unreachable by mouse.

Keyboard

Key Action
Enter Confirm
Cmd/Ctrl + Enter Confirm — works in every control type
Escape Cancel and restore the previous value
Tab Reach the trigger in every affordance style

In a textarea Enter writes a line, so it does not commit — Cmd/Ctrl + Enter does. That is also why the confirm and cancel buttons cannot be switched off for that control: without them and without knowing the shortcut, a reader would have no way to save at all, and the remaining exit discards.

Both Enter and Escape stop where they are handled. Without that, Escape inside a modal would close the modal as well as canceling the edit, and Enter inside a <form wire:submit> would submit the whole form instead of committing one field.

Where focus goes

Focus is decided by how the reader left, not by whether the save worked:

Leaving by Focus
Confirming with the keyboard Back to the trigger
Canceling / Escape Back to the trigger
Confirming with the mouse Back to the trigger
Clicking away without confirming Stays where the reader clicked — pulling them back would undo the move they just made
A save that failed Does not move — the error is here, and moving away hides it

What you can edit

Four built-in types, chosen with control. Each one edits in place the same way — only the control inside the open editor differs.

control Editor Reach for it when Note
text (default) Single-line input Titles, names, labels, short free text
textarea Multi-line input Descriptions, notes, anything wrapping The editor grows downward, and the buttons stay at the top edge next to where you are typing
number Numeric input Quantities, prices, counts Pass min, max and step straight through
select Dropdown A value from a fixed set Needs options as key => label, and displayValue for what read mode shows

Anything else goes through the editor slot — a date picker, a tags input, a combobox — so an uncovered type never has to wait for a release.

The four built-in types

select stores a key and shows a label, so read mode needs displayValue — otherwise it prints review where you meant In review. The same applies whenever the stored value is not the readable one: an amount stored as 1299, a date stored as an ISO string.

Bringing your own control

The editor slot covers everything the four types above do not, so an uncovered type never has to wait for a release:

<x-wirekit::inline-edit name="tags" label="Tags" :value="$post->tags">
    <x-slot:editor>
        <x-wirekit::tags-input x-ref="control" x-model="draft" />
    </x-slot:editor>
</x-wirekit::inline-edit>

The slot compiles inside the component's own Alpine scope, so draft, commit() and cancel() resolve with nothing threaded in. In exchange there is a contract, and it is checked rather than assumed — in development a missing ref is reported by name:

  1. Bind x-model="draft". That is the value the component commits.
  2. Set x-ref="control". Focus placement and the accessibility wiring both aim at it; without it they simply do nothing.
  3. Handle Escape yourself if your control is composite (a combobox, a multi-select). Two handlers for one key make one keypress do two things — the first Escape should close your list, the second should cancel the edit.
  4. Label it once. Either your control carries the label or this component does. Both means a duplicate announcement.

Do not drop this into a data-grid cell. Inside a role="grid" the arrow keys belong to the grid's own navigation, so a control that claims them fights the table and one keypress does two things. That conflict must be resolved before the component belongs in a grid cell.

Props

Prop Type Default Description
name string null Field name, carried in every event detail so several editors can be told apart
label string|null null Rendered in both modes, so the field is never unlabelled while reading
value string '' The current value
context string|null null Names the record in the trigger's accessible name
trigger string 'always' always · hover · focus-only
openOnValueClick bool true Clicking the value opens the editor
exclusive bool true Opening this editor closes any other open one
hint string|null null Help text, linked via aria-describedby
error string|null null Error text, linked via aria-describedby
control string 'text' text · textarea · number · select
options array [] Key => label pairs for control="select"
displayValue string|null null What read mode shows when the stored value is not the readable one. Resolves itself for a select
emptyText string|null null Read-mode text when there is no value
commitOn string 'explicit' explicit · blur. Forced to explicit for a select
actions bool true Show confirm/cancel. Cannot be disabled for a textarea
size string 'md' sm · md · lg
loading bool false Hold the saving state from the server
loadingTarget string|null null Scope the loading state to one action (loadingTarget, not target)
width string 'full' full takes the container width so read and edit boxes match; auto measures the content
scope string|null null Scoped personalization key

context matters more than it looks. In a list of twenty rows, twenty triggers named Edit title are twenty identical entries in a screen reader's control list. context="Q3 planning" makes each one say which record it belongs to.

Accessibility

  • The label renders in both modes and sits outside the part that toggles, so the field is never unlabelled while reading and the label does not jump into place on open
  • The trigger is a real <button type="button"> with a 44×44 touch target
  • The value is a role="presentation" container, never a button — so its accessible name is not the value text
  • aria-describedby composes the hint and error ids, and is never emitted empty
  • Confirm and cancel use aria-disabled while saving, never disabled — a disabled button leaves the tab order mid-interaction, which loses the reader's place exactly while they wait to find out whether it worked
  • A second Enter during a save does not submit again
  • The save outcome is announced through a polite live region

Keyboard Interaction

Key Action
Tab Move to the trigger, then between confirm and cancel while editing
Enter Confirm
Cmd/Ctrl + Enter Confirm
Escape Cancel, restoring the previous value

Pitfalls

  • Do not bind wire:model to the draft. A two-way binding writes on every keystroke, which leaves nothing to confirm and nothing for cancel to restore. The draft is local until the reader commits.
  • Do not infer success from the request finishing. Dispatch the saved event; a validation failure completes a request too.
  • Add wire:key when rendering these in a loop, so a morph keeps each editor attached to its own row.

Design Tokens

Token Purpose
--color-wk-bg-input Editor background
--color-wk-border-strong Editor border
--color-wk-text Value and label text
--color-wk-text-muted Trigger, hint, and the empty-value placeholder
--color-wk-danger-text Error text
--color-wk-success-text Confirm control
--radius-wk-md Editor corner radius
--ring-wk-width Focus ring

Usage & Conventions

The component owns the editing interaction and nothing else. Saving, validation, authorization and optimistic updates stay in your Livewire component, which is why the event carries name, value and previous and then gets out of the way.