A toast notification is one of those UI patterns that sounds simple until you try to build a good one. The element needs to appear smoothly, sit on top of everything else, and either auto-dismiss after a few seconds or let the user close it manually. With Alpine.js and Tailwind CSS, you can build a fully working version in a single HTML file — no build step required for a prototype, and easy to drop into any Astro or plain HTML project.
This guide walks through building an "update available" toast specifically — the kind that tells a visitor the site or app has new content and gives them a button to reload. By the end you will have a reusable component with smooth enter/leave transitions, auto-dismiss with a configurable delay, a manual close button, and a Tailwind v4 CSS-first setup that is easy to extend.
Prerequisites
You need Alpine.js (v3) and Tailwind CSS v4 loaded in your page. The simplest way to prototype is via CDN:
<!-- Tailwind CSS v4 via CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Alpine.js v3 via CDN -->
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
For a project using npm, install both packages and add @import "tailwindcss" to your main CSS file. That single import replaces the old @tailwind base/components/utilities directives in Tailwind v4.
The data model
Alpine's x-data directive defines the component's reactive state. For a toast you need very little: whether it is visible, and a method to dismiss it. Keep the dismiss method separate from simply setting show = false so you can add side effects later (removing from the DOM, logging analytics, etc.).
x-data="{
show: false,
init() {
// Show the toast shortly after the page loads
setTimeout(() => { this.show = true }, 800)
// Auto-dismiss after 8 seconds
setTimeout(() => { this.dismiss() }, 8000)
},
dismiss() {
this.show = false
}
}"
The init() method runs automatically when Alpine initialises the component. The 800 ms delay before showing prevents the toast from flashing in during the browser's initial paint. The 8-second auto-dismiss gives the user enough time to read the message and decide whether to act.
Showing and hiding with x-show
x-show toggles an element's visibility based on a boolean expression. When the expression is false, Alpine sets display: none on the element. When it becomes true, Alpine removes that inline style.
<div x-show="show">
This content shows and hides reactively.
</div>
On its own, x-show is abrupt — the element snaps in and out. That is where x-transition comes in.
Smooth transitions with x-transition
Alpine's x-transition directive accepts six phase-specific sub-directives that map directly to CSS class names applied at different moments in the enter and leave lifecycle:
x-transition:enter— classes active for the full enter phasex-transition:enter-start— classes applied the frame before the element appears (the "from" state)x-transition:enter-end— classes applied after the element appears (the "to" state)x-transition:leave— classes active for the full leave phasex-transition:leave-start— classes applied the frame before the element disappearsx-transition:leave-end— classes applied after the element disappears
Tailwind's transition utilities (transition, duration-300, ease-out) do the actual animation work. Alpine just applies and removes classes at the right moments.
For a toast sliding up from the bottom-right corner, a translate + opacity pair works well:
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
On enter the element starts invisible and shifted down 1 rem, then transitions to fully visible at its natural position over 300 ms. On leave the reverse happens in 200 ms (leave transitions are typically faster — the user has already read the message).
The full component
Here is the complete update notification toast. It sits fixed in the bottom-right corner, slides up on appear, includes a "Reload" action and a close button, and self-dismisses after 8 seconds.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Update Toast</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
</head>
<body class="bg-gray-100 min-h-screen">
<!-- Trigger button for testing -->
<div class="p-8">
<button
onclick="document.querySelector('[x-data]').__x.$data.show = true"
class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700"
>
Simulate update
</button>
</div>
<!-- Toast component -->
<div
x-data="{
show: false,
init() {
setTimeout(() => { this.show = true }, 800)
setTimeout(() => { this.dismiss() }, 8000)
},
dismiss() {
this.show = false
}
}"
x-show="show"
x-cloak
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
class="fixed bottom-6 right-6 z-50 w-80 bg-white rounded-xl shadow-lg ring-1 ring-gray-200 p-4"
role="alert"
aria-live="polite"
>
<div class="flex items-start gap-3">
<!-- Icon -->
<div class="shrink-0 mt-0.5">
<svg class="w-5 h-5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</div>
<!-- Text -->
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-gray-900">Update available</p>
<p class="mt-0.5 text-sm text-gray-500">A new version of this page is ready. Reload to see the latest content.</p>
<!-- Actions -->
<div class="mt-3 flex items-center gap-3">
<button
onclick="window.location.reload()"
class="text-sm font-medium text-blue-600 hover:text-blue-500"
>
Reload now
</button>
<button
@click="dismiss()"
class="text-sm font-medium text-gray-400 hover:text-gray-500"
>
Dismiss
</button>
</div>
</div>
<!-- Close button -->
<button
@click="dismiss()"
class="shrink-0 rounded-md p-1 text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Close notification"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</body>
</html>
Preventing flash with x-cloak
There is a brief window between when the browser renders the page and when Alpine initialises. Without x-cloak, an element with x-show="false" will briefly appear before Alpine hides it. Add the attribute to the toast element and a one-line CSS rule to your stylesheet:
[x-cloak] { display: none !important; }
Alpine removes the x-cloak attribute as soon as it initialises the component, so the element will only appear when Alpine is ready and the show expression is true.
Pausing auto-dismiss on hover
A useful UX improvement: stop the countdown while the user is reading the toast. This requires storing the timer ID and cancelling it on mouseenter, then restarting it on mouseleave.
x-data="{
show: false,
timer: null,
init() {
setTimeout(() => { this.show = true }, 800)
this.startTimer()
},
startTimer() {
this.timer = setTimeout(() => { this.dismiss() }, 8000)
},
pauseTimer() {
clearTimeout(this.timer)
},
dismiss() {
this.show = false
}
}"
@mouseenter="pauseTimer()"
@mouseleave="startTimer()"
Add the two event listeners to the same element that has x-data. Now hovering over the toast freezes the auto-dismiss clock, and moving the cursor away restarts it with a fresh 8-second window.
Adapting for Tailwind CSS v4 projects
If your project uses Tailwind v4 with the CSS-first setup, custom design tokens go in a @theme block in your main CSS file rather than a tailwind.config.js. For example, to define a custom shadow for the toast:
@import "tailwindcss";
@theme {
--shadow-toast: 0 4px 24px -2px oklch(0 0 0 / 0.12), 0 2px 8px -2px oklch(0 0 0 / 0.08);
}
This creates a shadow-toast utility class you can use on the toast element. The same pattern works for any custom colour, radius, or spacing token — define it once in @theme, use it as a regular Tailwind class everywhere.
Triggering toasts from JavaScript events
A real update notification typically fires in response to a service worker detecting a new cache version, or a polling request that returns a newer content hash. Alpine's $dispatch and x-on directives make it straightforward to decouple the trigger from the component. Add a listener on the window:
<div
x-data="{ show: false, ... }"
x-on:show-update-toast.window="show = true; startTimer()"
...
>
Then from your service worker registration or polling code, fire the event:
// When the service worker has a new version waiting
registration.addEventListener('updatefound', () => {
window.dispatchEvent(new CustomEvent('show-update-toast'))
})
The toast component stays purely declarative and HTML-focused; the business logic that decides when to show it lives entirely in your JavaScript module.
Accessibility considerations
Two attributes on the toast wrapper matter for screen readers. role="alert" tells assistive technology that this is an important, time-sensitive message. aria-live="polite" causes the content to be announced when it appears without interrupting whatever the user is currently doing. If your toast is truly urgent (a critical error, a session expiry warning), use aria-live="assertive" instead — but use that sparingly because it interrupts the current reading.
Also ensure the close button has a descriptive aria-label as shown in the example above. An icon-only button with no label reads as nothing to a screen reader user.
Wrapping up
You now have a complete, accessible update notification toast built entirely from Alpine.js directives and Tailwind utilities — no component library, no extra dependencies. The pattern scales naturally: swap the icon and copy for a success toast, an error banner, or a cookie consent notice. The x-data / x-show / x-transition trio handles every show/hide animation case the same way, so once you understand one toast you can build all of them.