Skip to main content
Copy for LLM

Toast

Event-driven notifications that appear temporarily to confirm actions or surface warnings. Toasts auto-dismiss after a configurable duration and stack vertically in a fixed corner of the viewport.

Basic Usage

Mount the toast region once per layout (typically in your app shell):

{{-- In your layout file --}}
<x-wirekit::toast-region />

The region listens on window, so anything that dispatches the event reaches it — a Livewire component after the write succeeds, or Alpine's $dispatch for a purely client-side action.

Most toasts report the outcome of something the server did, so that is the shape shown here: the button calls a Livewire action, and the toast is dispatched after the save returns. Nothing in the markup decides what the toast says.

Trigger a toast

Variants

Four semantic variants control color, icon, and ARIA behavior. Click each button to see the toast:

Toast variants

Position

Control where toasts appear on the screen:

<x-wirekit::toast-region position="top-right" />    {{-- default --}}
<x-wirekit::toast-region position="top-left" />
<x-wirekit::toast-region position="top-center" />
<x-wirekit::toast-region position="bottom-right" />
<x-wirekit::toast-region position="bottom-left" />
<x-wirekit::toast-region position="bottom-center" />

Each region below uses a scoped name prop so it only listens on its own channel — avoiding cross-talk with other regions on the same page:

Top-left position
Bottom-right position

Duration

Toasts auto-dismiss after 5 seconds by default. Override per-region or per-toast:

{{-- All toasts in this region dismiss after 3 seconds --}}
<x-wirekit::toast-region :duration="3000" />
// Per-toast override (0 = persistent, must be manually dismissed)
$dispatch('wirekit-toast', {
    title: 'Permanent',
    message: 'This toast stays until dismissed.',
    duration: 0
})

Queue Limit

Limit how many toasts are visible at once. Oldest toasts are removed when the limit is reached:

<x-wirekit::toast-region :max="3" />

Scoped Regions

By default, all toast regions listen on the global wirekit-toast event. When you have multiple regions (e.g. one for system notifications, one for form feedback), use the name prop to scope each region to its own event channel. This prevents duplicate toasts appearing in every region.

{{-- System notifications — listens on 'wirekit-toast-system' --}}
<x-wirekit::toast-region name="system" position="top-right" />

{{-- Form feedback — listens on 'wirekit-toast-form' --}}
<x-wirekit::toast-region name="form" position="bottom-right" />

Dispatch to a specific region by targeting its scoped event name:

// Only appears in the "form" region
$dispatch('wirekit-toast-form', {
    variant: 'success',
    title: 'Saved',
    message: 'Your changes were saved.'
})

// Only appears in the "system" region
$dispatch('wirekit-toast-system', {
    variant: 'warning',
    title: 'Maintenance',
    message: 'Scheduled downtime in 30 minutes.'
})

Regions without a name prop continue to listen on the default wirekit-toast event, so existing code works unchanged.

eventScope — DOM-containment filter (alternative to name)

When you can't (or don't want to) coordinate distinct event names — for example because every region on the page emits the same wirekit-toast event but each region should only handle events from its own portion of the DOM — set eventScope to a CSS selector. The region then ignores any event whose dispatching element doesn't sit inside an ancestor matching the selector:

{{-- Per-section toast surface: only handles events dispatched from inside the wrapper --}}
<div data-toast-section>
    <x-wirekit::toast-region eventScope="[data-toast-section]" position="top-right" />

    {{-- Buttons in this section dispatch the standard event; only THIS region picks it up --}}
    <x-wirekit::button @click="$dispatch('wirekit-toast', { variant: 'success', message: 'Saved' })">
        Save
    </x-wirekit::button>
</div>

name (event-name routing) and eventScope (DOM-containment filtering) are independent and may be combined — set both to require both a matching event name AND a dispatcher inside the scope wrapper. Default null means no containment filter (every dispatched event of the matching name is handled — the existing global-listener behavior).

This is the cleanest pattern for "per-card local toast queues" and similar layouts where the section that emits the toast is the section that should display it.

Pause on Hover

Auto-dismiss pauses when the user hovers over a toast and resumes when they move away. This is built-in — no configuration needed.

Dispatch Payload

The wirekit-toast event accepts:

Property Type Default Description
title string null Bold heading
message string '' Body text
variant string 'info' info | success | warning | danger
duration number Region default Auto-dismiss in ms (0 = persistent)

After a Redirect

A toast lives in the page it was dispatched on, so a redirect right after it takes the toast away: a full redirect unloads the page, and a wire:navigate visit swaps the body the region sits in. Two ways carry it across.

A wire:navigate redirect keeps the region when your layout wraps it in @persist. The toast your action dispatches before the redirect stays on screen through the visit:

{{-- 1. In the layout: one region, kept across wire:navigate visits. --}}
@persist('wirekit-toasts')
    <x-wirekit::toast-region />
@endpersist
public function save(): void
{
    // 1. Store the title on the post this component edits
    $this->post->update(['title' => $this->title]);

    // 2. Dispatch the toast first: the persisted region keeps it through the visit
    $this->dispatch('wirekit-toast', message: __('Post saved'), variant: 'success');

    $this->redirectRoute('posts.index', navigate: true);
}

A full redirect unloads the page, so flash the toast to the session and dispatch it on the next page, after the region, which is listening by then. This works for a wire:navigate redirect too:

public function save(): void
{
    // 1. Store the title on the post this component edits
    $this->post->update(['title' => $this->title]);

    // 2. Flash the toast for the next page
    session()->flash('toast', ['message' => __('Post saved'), 'variant' => 'success']);

    $this->redirectRoute('posts.index');
}
{{-- 1. In the layout, after the toast region. --}}
@if (session()->has('toast'))
    <div x-data x-init="$dispatch('wirekit-toast', @js(session('toast')))"></div>
@endif

The snippet needs Alpine's default build: under the CSP build an expression may not call JSON.parse, which @js() writes.

Width & Layout

Individual toasts have a fixed width of w-80 (20rem / 320px), capped to the viewport width minus 2rem on narrow screens (max-w-[calc(100vw-2rem)]). Position is controlled by the position prop on the toast region — not by CSS classes. Stack direction and spacing are handled automatically.

Props

Prop Type Default Description
position string 'top-right' Where toasts appear. One of: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right
duration number 5000 Default auto-dismiss duration in ms
max number 5 Maximum visible toasts (oldest removed when exceeded)
name string null Scoped name — listens on wirekit-toast-{name} instead of global wirekit-toast
label string null Accessible name for the region, and the switch that makes it a landmark (role="region"). Without it the region is not a landmark at all — set it only if every region on the page gets its own name
eventScope string null CSS selector — when set, only events whose dispatching element is inside an ancestor matching the selector are handled (DOM-containment filter, complements name's event-name routing)
filled bool false Use full variant background color instead of light tint (like a filled callout)
scope string null Scoped personalization key

Accessibility

  • The announcement is separate from the toast. The region renders two permanently-present visually-hidden live regions — one aria-live="polite", one aria-live="assertive", both aria-atomic="true" — and a new toast writes its text into one of them. A live region has to exist before the text it carries: an element that arrives already holding its message is a new node, not a region that changed, and assistive technology stays silent.
  • The toast card itself carries no live role and no aria-live. That is deliberate rather than missing. role="alert" implies aria-live="assertive", so putting it back on a card created inside the loop would either announce nothing (best case) or have the message read twice (worst case). The card is ordinary content, reachable and readable like any other.
  • danger goes to the assertive slot; everything else — including warning — goes to the polite one. Assertive interrupts whatever the reader is in the middle of, and a notice that dismisses itself after five seconds rarely earns that. A warning that genuinely must be acknowledged does not belong in a toast at all (see Pitfalls below); it belongs in <x-wirekit::alert-dialog> or an inline <x-wirekit::alert>, where nothing takes it away.
  • Each toast has a dismiss button labeled aria-label="Dismiss notification", translated through WireKit's own catalog.
  • Toasts never steal focus when they appear.
  • They do not hide the focus either. While a toast shows, the region marks the band its stack covers, and the page brings a control a reader tabs to out from under it (keeping the focus clear of a pinned bar, WCAG 2.4.11). At the very top or bottom of the page nothing is left to scroll, so there the stack moves to the other edge while a control under it has the focus, and comes back once the focus moves on.
  • A reader who needs longer than the default can be given it. Hover and focus pause the timer, and the region's duration sets the time for every toast it shows, so an application that lets its readers choose more time binds it to that setting: :duration="$user->toastSeconds * 1000", or 0 to keep each toast until it is dismissed. WCAG 2.2.1 asks for a way to turn off or lengthen a time limit like this one.
  • A toast region stays reachable while a modal dialog is open. The page behind an open modal, drawer, alert-dialog, command-palette, lightbox or tour is inert, and the region is kept out of it, so a toast raised under a dialog is still announced.
  • Decorative icons are aria-hidden="true".
  • Pass label to expose the whole region as a named landmark (role="region"). Without it the region is not a landmark — an unnamed one is worse than none, and several regions on a page sharing one built-in name is what landmark-unique reports.

Keyboard Interaction

The toast card is not focusable, but the dismiss button inside it is.

Key Action
Tab Move to the next dismiss button in the stack
Enter / Space Dismiss the focused toast

Two behaviors follow from that, and both are what a reader working the stack by keyboard needs:

  • Focus pauses the auto-dismiss timer, the same way hovering does — otherwise a toast disappears mid-reach.
  • Dismissing a toast hands focus to the next one (or to the previous one at the bottom of the stack, or back to wherever you tabbed in from once the stack is empty). Focus is never dropped to the top of the document.

Pitfalls

  • Don't use a toast for errors that block progress. Toasts auto-dismiss; errors that the user must acknowledge belong in <x-wirekit::alert-dialog> or an inline <x-wirekit::alert>.
  • Keep the stack short. Stacking degrades into noise. The region's queue dispatcher enforces the limit itself — max defaults to 5 and drops the oldest beyond it — so custom dispatch logic does not need to count, but a screen showing five at once is already telling the reader too much at once.

Design Tokens

Toasts use the same tinted-background approach as Alert for visual consistency:

Token Usage
--color-wk-info-tone / --color-wk-info-text Info variant border tint / icon; a filled info toast writes in --color-wk-info-fg
--color-wk-success Success variant
--color-wk-warning Warning variant
--color-wk-danger Danger variant
--shadow-wk-lg Toast card elevation
--radius-wk-md Card corner radius
--padding-wk-y-md Card padding, on all four sides
--font-wk-sans Font family
--transition-wk-duration Enter/leave animation timing

Customization

Override toast styles via personalization. A closure is handed each block as it ships and changes only what it names:

WireKit::personalize('toast-region', [
    // More room from the edge of the viewport and between the toasts. The padding comes first:
    // str_replace() works through the pairs in order, and `p-4` is also part of `gap-4`.
    'base' => fn (string $vendor): string => str_replace(['p-4', 'gap-3'], ['p-6', 'gap-4'], $vendor),
    // Wider, rounder toasts with a deeper shadow.
    'toast' => fn (string $vendor): string => str_replace(
        ['w-80', 'rounded-[var(--radius-wk-md)]', 'shadow-[var(--shadow-wk-lg)]'],
        ['w-96', 'rounded-xl', 'shadow-2xl'],
        $vendor,
    ),
]);

A plain string would replace the block, and these blocks hold more than their look. The region lets clicks through to the page (pointer-events-none) and every toast takes them back (pointer-events-auto), so a toast built from a string alone ignores clicks, its close button included. The toast block also caps the width at the viewport's on a phone and lays out the icon, the text and the close button.

Further Reading

  • Overlay Events — the canonical event vocabulary for showing, closing and reacting to every overlay, with payload shapes and Livewire-side dispatch examples.

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store