Skip to main content
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 previews on this page cannot save, and that is the component working correctly. Try it: type a new title, confirm, and the value snaps back. There is no Livewire component behind a documentation preview, so nothing answers the confirmation — and this component only shows a value as stored once something tells it so. After about ten seconds it stops waiting and reports the save as unconfirmed, keeping your text in the field rather than discarding it.

That is the whole contract in one interaction. In your app the value stays because your handler says so, not because the editor closed.

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.

When nothing answers

The confirmation is an event. If your handler never dispatches wirekit:inline-edit-saved or wirekit:inline-edit-failed — you forgot the paired event, the request died, the tab lost its connection — the editor would otherwise stay busy forever.

It does not. After save-timeout milliseconds (default 10000) the component stops waiting and reports, in its live region, that the save was not confirmed. It says exactly that, and deliberately not that it failed: a missing acknowledgment is not evidence that the write did not happen, and telling a reader it failed invites them to edit a value that was already stored.

The editor stays open with the text still in it. Nothing the reader typed is thrown away on the strength of a guess.

Prop Default Effect
save-timeout 10000 Milliseconds to wait for an answer. 0 waits indefinitely — only sensible when the answer is guaranteed.
unknown-message a built-in sentence What the live region says when the bound is reached.

announce-error="false" silences this report too — the give-up message follows the same switch as the error message, so a page that opts out of being told about deviations never hears about this one either. That is one decision rather than two, and it is easy to make by accident while thinking only about validation text.

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

Clicking away also discards the draft

Leaving the control without confirming closes the editor and drops what was typed, restoring the value the field had before. It is the same outcome as Escape, reached a different way. The rich-text recipe is the one exception: it binds no blur, so its draft stays open.

That is the reason commitOn exists. The default explicit means nothing is written until the reader confirms — so a click into the next field cannot save half a thought, and the price of that guarantee is that the half-thought is not kept either. Set commitOn="blur" when moving away should save instead of discard.

A round trip is not a departure. A wire:poll, a save elsewhere on the page, or any other morph blurs a focused control for an instant while the DOM is patched, and the editor stays open with the draft intact — see Surviving a morph. Only a reader who really moves focus somewhere else loses it.

A draft only ever lives in the browser. Nothing about it reaches the server before the reader confirms, so a reload, a wire:navigate, or a closed tab discards it too. Before a navigation that would throw one away the browser asks first — that prompt is the only warning there is, and it cannot appear for a click inside the same page.

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. It works on a WireKit component as well as on a plain element: focus then goes to the part of the component that takes text, and the hint and error are attached there.
  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. With label and no name of its own, the part of your control that takes focus is named by the visible label when the editor opens, through aria-labelledby, and a click on the label focuses it while the editor is open. A control that names itself keeps its name: an aria-label, an aria-labelledby or a <label> of its own wins, the way a caller's aria-label wins on a built-in field. The editor always names itself, so the recipe below gives it the label's words.
  5. Keep it out of a surrounding form while the editor is closed. The editor stays on the page while the value is shown, and a form checks every field it owns, shown or not. A required control of yours with an empty draft would stop a form around the component from submitting, and the browser cannot focus a hidden field to say why. Bind its disabled to the open state, as the built-in controls do: x-bind:disabled="!editing".

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.

Rich text

Rich text is not one of the four built-in types, and that is deliberate. Its editing mode is a full editor with a toolbar and its read mode is a rendered document, so it is composed through the slot rather than switched on with control. Two pieces do the work: the editor in the editor slot, and the document handed to read mode as markup.

Rich text through the editor slot

Version two ships faster exports.

  • A dark theme
  • Keyboard shortcuts

The slot's contract applies as written. x-ref="control" on the editor puts focus in its editing surface, x-model="draft" binds the document in both directions, and aria-label gives the editor the same words the visible label shows. clean() comes from mews/purifier, the sanitizer the editor page recommends. Beyond the contract, four things matter:

  1. wire:ignore goes on the editor's mount node, and nowhere above it. The editor builds its editing surface in the browser, after the server has rendered the page. Without the marker, a Livewire round trip from anywhere else on the page puts that node back to what the server rendered, and the editing surface disappears along with the unsaved text in it. On the inline-edit itself the marker would also stop read mode from updating: the save reaches your component, and the reader keeps looking at the old document.
  2. Read mode gets the document as markup. Passed to displayValue as an Htmlable, the document renders in the editor's own typography, so it keeps its look when the editor opens. A document with no text in it, such as the <p></p> an emptied editor produces, shows emptyText. Read mode renders the HTML as it is, which is why the save above sanitizes it.
  3. Enter writes a paragraph, so the recipe binds the other two keys. Escape cancels and Cmd/Ctrl + Enter confirms. The confirm binding runs in the capture phase and stops there: Tiptap's starter kit binds the same shortcut to a line break, and bound any later, the break would land in the document just before it is sent.
  4. Clicking elsewhere keeps the draft. The recipe binds no blur, so a paragraph of unsaved text survives a click somewhere else on the page, which is where it differs from the built-in controls. Confirm, cancel and Escape close the editor, and so does opening another one, as described in Several editors at once.

Error state

An error re-opens the editor, which is why the preview below is already in edit mode with nothing clicked. A rejected save arrives as a Livewire morph, and a morph replaces the open editor with read mode — so without this the reader is dropped back to the value they tried to change, holding an error about a field they can no longer see. The message is linked with aria-describedby, so it reaches the control rather than sitting beside it.

The error can come from either side: the error prop, or the validation bag under the field's name. Livewire reports a failed save() through the bag, so both are read.

Error state — the editor is open because the error opened it

Click the value to edit it in place.

A project with that title already exists.

Props

Prop Type Default Description
required bool false Marks the field as required. Forwarded to the composed control, which emits the native attribute, and renders the label indicator (*).
name string null Field name, carried in every event detail so several editors can be told apart. The control inside takes no name, so a surrounding form submits nothing of it: the confirmed value reaches your component through the events
label string|null null Rendered in both modes, so the field is never unlabeled while reading
id string|null auto-generated The editor field's id. A <label for> of yours with this id names the field. With the editor slot the field is yours, so put the id on your control
value string|BackedEnum '' The current value. A backed enum is its 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
help string|null null An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label
error string|false|null null Error text, linked via aria-describedby. A non-empty value opens the editor — see Error state. false shows none and leaves the $errors bag alone, see Turning an Error Off
control string 'text' text · textarea · number · select
rows string|int 'auto' Textarea height. auto sizes to content, so the editor opens as tall as the text it replaces and grows while typing; a row count pins a fixed height. Ignored by every other control
saveTimeout int 10000 Milliseconds to wait for a saved / failed answer before giving up. 0 waits indefinitely
unknownMessage string|null null What the live region says when that bound is reached. Defaults to a sentence reporting the save as unconfirmed
options array [] Key => label pairs for control="select"
displayValue string|Htmlable|null null What read mode shows when the stored value is not the readable one. Resolves itself for a select. An Htmlable, such as str($html)->toHtmlString(), renders as markup in the typography of the editor, and a document with no text in it counts as empty. See Rich text
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
disabled bool false Takes the field out of use: the value stays readable and dimmed, the pencil is disabled, a click on the value opens nothing, and a surrounding form submits nothing of it. A control in the editor slot is yours to disable
readonly bool false Read-only: the value stays readable and is not dimmed, and nothing opens the editor. There is no pencil
loadingTarget string|null null Scope the loading state to one action (loadingTarget, not target)
announceError bool|null null Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region.
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.

Slots

Slot Description
editor A control of your own for the editing mode, for a type the built-in ones do not cover. See Bringing your own control.
labelAfter Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label.

Accessibility

  • The label renders in both modes and sits outside the part that toggles, so the field is never unlabeled 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
  • An aria-label or aria-labelledby of yours names the built-in control, not the wrapper around it
  • An inputmode, enterkeyhint, autocapitalize, spellcheck, maxlength, autocomplete or placeholder written on the tag goes to the built-in text, textarea or number control, a list to the text or number control, a min, max or step to the number control, and an autofocus goes to the trigger. A select takes none of them. A control of your own in the editor slot takes them on its own tag
  • 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
  • With disabled, the pencil is a disabled button and leaves the tab order, as a disabled native field does: nothing is under way that the reader could lose their place in. The value is still read as text
  • A second Enter during a save does not submit again
  • The save outcome is announced through a polite live region
  • While the editor is closed, its built-in field is disabled. The editor stays on the page, and a form checks every field it owns, shown or not: an empty required value, or a number off its step, would otherwise stop a form around the component from submitting with nothing on screen to say why

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

In the rich-text recipe Enter writes a paragraph, and the recipe binds Escape and Cmd/Ctrl + Enter itself.

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.

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.
  • Do not put wire:ignore on the inline-edit when composing rich text. It belongs on the editor's mount node inside the slot; on the root it stops read mode from showing what was saved. See Rich text.

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.

Updated in WireKit v2.69.0 (2026-10-18)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store