---
title: Unsaved Hint
description: Says beside the save button that a form holds values its server does not have yet, frames each unsaved field, and can ask before the reader leaves
visibility: guest
draft: false
---

# 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](/components/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

:::preview{title="A card with an unsaved field"}
<x-wirekit::card style="max-width: 28rem;">
    <x-wirekit::card.body>
        <x-wirekit::stack gap="md">
            <x-wirekit::input label="Supplier name" name="name" value="Meridian Supplies Ltd" data-wk-unsaved />
            <x-wirekit::input label="Contact person" name="contact" value="Sam Weber" />
        </x-wirekit::stack>
    </x-wirekit::card.body>
    <x-wirekit::card.footer>
        <x-wirekit::row justify="end" gap="sm" align="center">
            <x-wirekit::unsaved-hint data-wk-unsaved-shown />
            <x-wirekit::button>Save</x-wirekit::button>
        </x-wirekit::row>
    </x-wirekit::card.footer>
</x-wirekit::card>
:::

:::source{language="blade"}
<x-wirekit::card x-wk-unsaved>
    <x-wirekit::card.body>
        <x-wirekit::stack gap="md">
            <x-wirekit::input label="Supplier name" wire:model="name" />
            <x-wirekit::input label="Contact person" wire:model="contact" />
        </x-wirekit::stack>
    </x-wirekit::card.body>
    <x-wirekit::card.footer>
        <x-wirekit::row justify="end" gap="sm" align="center">
            <x-wirekit::unsaved-hint />
            <x-wirekit::button wire:click="save">Save</x-wirekit::button>
        </x-wirekit::row>
    </x-wirekit::card.footer>
</x-wirekit::card>
:::

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](/components/combobox) or a
[multi-select](/components/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:

:::preview{title="An unsaved field and an invalid one"}
<x-wirekit::stack gap="md" style="max-width: 24rem;">
    <x-wirekit::input label="Delivery note" name="note" value="Leave at the back door" data-wk-unsaved />
    <x-wirekit::input label="Order email" name="email" type="email" value="orders@" error="Enter a complete email address." data-wk-unsaved />
</x-wirekit::stack>
:::

:::source{language="blade"}
<x-wirekit::stack gap="md">
    <x-wirekit::input label="Delivery note" wire:model="note" />
    <x-wirekit::input label="Order email" type="email" wire:model="email" />
</x-wirekit::stack>
:::

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:

```css
[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.

```blade
<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:

```blade
<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:

:::preview{title="A sentence of your own"}
<x-wirekit::row justify="end" gap="sm" align="center" style="max-width: 28rem;">
    <x-wirekit::unsaved-hint text="Changes to this supplier are not saved yet" data-wk-unsaved-shown />
    <x-wirekit::button>Save</x-wirekit::button>
</x-wirekit::row>
:::

:::source{language="blade"}
<x-wirekit::row justify="end" gap="sm" align="center">
    <x-wirekit::unsaved-hint text="Changes to this supplier are not saved yet" />
    <x-wirekit::button wire:click="save">Save</x-wirekit::button>
</x-wirekit::row>
:::

## 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](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html)). The default border is above 3:1 against the field in the light and the dark theme ([WCAG 1.4.11](https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html)).
- 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](#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](/components/form) for the error-announcement policy of the controls inside
- [Card](/components/card) for the footer the hint usually sits in
