Feedback
toast
Toast region; dispatch a `toast` window event to show one.
<div>
<x-ui.button variant="outline"
onclick="window.dispatchEvent(new CustomEvent('toast', { detail: { title: 'Saved', description: 'Your changes have been saved.' } }))">
Show toast
</x-ui.button>
<x-ui.toast />
</div>
Installation
php artisan ui:add toast
1. Install dependencies
- composer:
gehrisandro/tailwind-merge-laravel
2.
Copy the source into
resources/views/components/ui/
resources/views/components/ui/toast.blade.php
@props([
'position' => 'bottom-right',
])
@php
$anchor = match ($position) {
'top-right' => 'top-4 right-4',
'top-left' => 'top-4 left-4',
'bottom-left' => 'bottom-4 left-4',
default => 'bottom-4 right-4',
};
$regionClasses = \TailwindMerge\Laravel\Facades\TailwindMerge::merge(
'fixed z-[100] flex flex-col gap-2 w-full max-w-sm',
$anchor,
$attributes->get('class'),
);
@endphp
<div data-ui-toast-region class="{{ $regionClasses }}" role="region"
aria-live="polite" {{ $attributes->except('class') }}>
{{-- Cloned once per toast; edit the markup here to restyle them. --}}
<template data-ui-toast-template>
<div role="status"
class="pointer-events-auto flex translate-y-1 items-start gap-3 rounded-lg border bg-background p-4 text-foreground opacity-0 shadow-lg transition-all duration-200 data-[state=visible]:translate-y-0 data-[state=visible]:opacity-100">
<div class="flex-1">
<p class="text-sm font-semibold" data-ui-toast-title></p>
<p class="hidden text-sm text-muted-foreground"
data-ui-toast-description></p>
</div>
<button type="button"
class="text-muted-foreground hover:text-foreground"
aria-label="Dismiss" data-ui-toast-close>×</button>
</div>
</template>
</div>
@once
<script>
(function() {
// The Blade once-directive above keeps this to a single copy per
// page, except when a component is rendered into a slot that is
// echoed twice, the way the sidebar does for its desktop and its
// mobile panel. Then the markup ships twice and every click would
// fire the handler twice, so this flag is the real guard.
if (window.__laralcnToast) return;
window.__laralcnToast = true;
function dismiss(toast) {
toast.dataset.state = 'hidden';
setTimeout(function() {
toast.remove();
}, 200);
}
function show(region, detail) {
var template = region.querySelector('[data-ui-toast-template]');
var toast = template.content.firstElementChild.cloneNode(true);
toast.querySelector('[data-ui-toast-title]').textContent = detail.title || '';
var description = toast.querySelector('[data-ui-toast-description]');
description.textContent = detail.description || '';
description.classList.toggle('hidden', !detail.description);
region.appendChild(toast);
// Flush the hidden frame, then reveal in the same tick, so the
// toast still shows in a background tab (no requestAnimationFrame).
toast.getBoundingClientRect();
toast.dataset.state = 'visible';
setTimeout(function() {
dismiss(toast);
}, detail.duration || 4000);
}
window.addEventListener('toast', function(event) {
document.querySelectorAll('[data-ui-toast-region]').forEach(function(region) {
show(region, event.detail || {});
});
});
document.addEventListener('click', function(event) {
var close = event.target.closest('[data-ui-toast-close]');
if (close) dismiss(close.closest('[role="status"]'));
});
})();
</script>
@endonce
Usage
<x-ui.button
onclick="window.dispatchEvent(new CustomEvent('toast', { detail: { title: 'Saved', description: 'Your changes have been saved.' } }))">
Show toast
</x-ui.button>
{{-- One region per page. position: bottom-right (default), bottom-left,
top-right, top-left or top-center. --}}
<x-ui.toast position="top-center" />
Ships its own inline <script>; no JS dependencies. Place once near </body>. Trigger with: window.dispatchEvent(new CustomEvent('toast', { detail: { title, description, duration } })). position: bottom-right|bottom-left|top-right|top-left.
Props
Every prop with its default. Pass any of them as attributes,
using : to bind a PHP value.
@props([
'position' => 'bottom-right',
])