Unsaved Hint
A form that holds what its server does not have yet says so. Put x-wk-unsaved on the element
around the fields, a <form> or a card, and <x-wirekit:: beside
the save button. While a field differs from the value the server last sent, the field is framed in
blue and the hint shows its sentence. Saving brings both back.
Basic Usage
The preview shows the moment after the reader changed the name. In your application the attributes
that draw it are set for you: x-wk-unsaved compares each field and marks the ones that differ.
What Counts as Unsaved
x-wk-unsaved asks the question wire:dirty asks: does the page hold a value for this property
that the Livewire component has not received? It compares every field inside it whose
wire:model has no modifier that sends the value by itself.
| The field | Tracked |
|---|---|
wire:model="name" |
Yes |
wire:model.number="quantity", or any modifier that changes how the value is read |
Yes |
wire:model.live, .blur, .change or .lazy |
No: the value reaches the server on its own |
| A file field | No |
Anything inside an element with data-wk-unsaved-off |
No |
A component whose wire:model sits on its own root, such as a combobox or a
multi-select, is tracked the same way. Its value lives in the
component's state rather than in the field, and that state is what is compared.
A field counts as saved once the server has the value, and any request of the component sends every
deferred change with it, not only the save. When a field with wire:model.live or a poll sends a
request, the other fields count as saved from then on, although nothing stored them. Keep a form
with an unsaved hint free of fields that send by themselves, or put those inside
data-wk-unsaved-off.
The Field
An unsaved field carries data-wk-unsaved, and the stylesheet gives it the border
--color-wk-border-unsaved, a blue, so it reads as neither an error nor a success. An
invalid field keeps its error border:
Enter a complete email address.
The scope carries data-wk-has-unsaved while any field in it is unsaved, for a style of your own,
such as a save button that stands out:
[data-wk-has-unsaved] .save-button {
box-shadow: 0 0 0 2px var(--color-wk-border-unsaved);
}
Asking Before the Reader Leaves
x-wk-unsaved. asks before the page is left with an unsaved field. A reload, a closed tab, a
typed address and an ordinary link get the browser's own question, in the browser's words. A
wire:navigate link never unloads the page, so the browser does not ask; there the scope asks the
hint's question, in the reader's language. A step back or forward through the history is not held.
<form wire:submit="save" x-wk-unsaved.confirm>
<x-wirekit::input label="Supplier name" wire:model="name" />
<x-wirekit::row justify="end" gap="sm" align="center">
<x-wirekit::unsaved-hint />
<x-wirekit::button type="submit">Save</x-wirekit::button>
</x-wirekit::row>
</form>
A save that redirects does not ask: by the time the redirect runs, the server has the values. A scope without a hint asks in English unless it names its own question:
<div x-wk-unsaved.confirm data-wk-unsaved-confirm="{{ __('Discard the changes to this supplier?') }}">
Your Own Sentence
text replaces the sentence, and the slot replaces it with markup:
The Directive
x-wk-unsaved takes no expression, and .confirm is its one modifier:
| Directive | On | Does |
|---|---|---|
x-wk-unsaved |
the element around the fields | Marks each unsaved field with data-wk-unsaved, itself with data-wk-has-unsaved, and shows the hints inside it |
x-wk-unsaved.confirm |
the same element | Also asks before the page is left with an unsaved field |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
text |
string|null | null |
The sentence shown while a field in the scope is unsaved. Without it the hint reads "Unsaved changes", in the reader's language |
question |
string|null | null |
What x-wk-unsaved.confirm asks before a wire:navigate visit. Without it: "You have unsaved changes. Leave this page anyway?", in the reader's language |
scope |
string|null | null |
Scoped personalization name |
Accessibility
- The hint is a polite live region (
role="status") that is always in the page. When the first field becomes unsaved, its sentence is written again, so a screen reader announces it once; it says nothing when a save clears it. - While nothing is unsaved, the hint takes no room and its sentence is out of the accessibility tree, so a screen reader does not find a stale "Unsaved changes" in the page.
- The frame of an unsaved field is a cue beside the sentence, not the only one: the hint says in words what the color shows (WCAG 1.4.1). The default border is above 3:1 against the field in the light and the dark theme (WCAG 1.4.11).
- The dot before the sentence is decorative (
aria-hidden).
Keyboard Interaction
The hint takes no focus and has nothing to operate: it is a sentence. The fields around it keep their own keys, and nothing about the keyboard changes when a field turns unsaved. With .confirm, the question before a wire:navigate visit is the browser's own dialog, answered with Enter or Escape.
Pitfalls
- The hint belongs inside the scope. A hint outside any
x-wk-unsavedstays hidden. - Any request counts as saving. See What Counts as Unsaved: a field with
wire:model.livein the same component makes the others count as saved with every request it sends. - A browser asks a reload in its own words. The text of that question cannot be set by a page;
questionreaches thewire:navigatecase only.
Design Tokens
| Token | Used for |
|---|---|
--color-wk-border-unsaved |
The border of an unsaved field, and the dot before the sentence |
--color-wk-text-muted |
The sentence |
--text-wk-sm |
The sentence's size |
--font-wk-sans |
The sentence's font |