Skip to main content
Copy for LLM

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::unsaved-hint> 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

A card with an unsaved field
Unsaved changes

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:

An unsaved field and an invalid one

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.confirm 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:

A sentence of your own
Changes to this supplier are not saved yet

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-unsaved stays hidden.
  • Any request counts as saving. See What Counts as Unsaved: a field with wire:model.live in 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; question reaches the wire:navigate case 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

See Also

  • Form for the error-announcement policy of the controls inside
  • Card for the footer the hint usually sits in

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store