Summit Themes
Blog

How to create an update notification toast with Alpine.js and Tailwind CSS

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 phase
  • x-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 phase
  • x-transition:leave-start — classes applied the frame before the element disappears
  • x-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.