Component

Persistent Toast

Toast that stays open until explicitly dismissed, with a shrinking progress bar showing the remaining time. The bar pauses on hover and resumes on leave. Ideal for upload results and important notices that shouldn't be missed.

  • Component
  • Tailwind
  • HTML
  • MIT

Preview

Live component

Interactive preview
Open preview
9:41 100%
devsnips.dev/library/tailwind/components/toasts/persistent-toast/
fluid · no fixed width 100%
No dependencies Focus-visible

Install

Add to your project

terminal
npx devsnips add Tailwind/Components/Toasts/persistent-toast
registry path: Tailwind/Components/Toasts/persistent-toast

Source

Implementation

code.html
<!--
Snippet Name: Persistent Toast
Description: Toast that stays open until explicitly dismissed, with a shrinking progress bar showing the remaining time. Great for upload/download progress or important notices.
Author: DevSnips Contributors
Usage Example: Call showToast() for a non-ignorable confirmation that still times out.
-->
<div
  id="persistent-toast-region"
  class="pointer-events-none fixed bottom-4 right-4 z-50 flex w-full max-w-sm flex-col items-end gap-3"
  role="region"
  aria-label="Notifications"
  aria-live="polite"
  data-toast-region
></div>

<!-- Demo trigger button -->
<button onclick="showToast()" class="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2">
  <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3 3-3"/></svg>
  Upload file
</button>

<script>
  (function () {
    const root = document.currentScript.closest('[data-toast-region]');
    if (!root) return;
    const DURATION = 6000;

    function dismiss(toast) {
      if (!toast || toast.dataset.dismissed === 'true') return;
      toast.dataset.dismissed = 'true';
      toast.classList.add('translate-x-2', 'opacity-0');
      toast.addEventListener('transitionend', () => toast.remove(), { once: true });
      setTimeout(() => toast.remove(), 400);
    }

    function showToast() {
      const toast = document.createElement('div');
      toast.className = 'toast-item pointer-events-auto relative w-full overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg ring-1 ring-black/5 transition duration-300 ease-out';
      toast.setAttribute('role', 'status');
      toast.innerHTML = `
        <div class="flex items-start gap-3 p-4">
          <span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-blue-100 text-blue-600">
            <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3 3-3"/></svg>
          </span>
          <div class="min-w-0 flex-1">
            <p class="text-sm font-semibold text-gray-900">Upload complete</p>
            <p class="mt-0.5 text-sm text-gray-500">design-system.fig was uploaded successfully.</p>
          </div>
          <button type="button" class="toast-close shrink-0 rounded-md p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-400" aria-label="Dismiss notification">
            <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
          </button>
        </div>
        <div class="toast-progress h-1 w-full origin-left scale-x-100 bg-blue-500 transition-transform ease-linear" style="transform: scaleX(1)"></div>`;

      root.appendChild(toast);

      const bar = toast.querySelector('.toast-progress');
      const closeBtn = toast.querySelector('.toast-close');

      // Start the countdown bar on the next frame so the transition runs
      function startBar() {
        requestAnimationFrame(() => {
          bar.style.transitionDuration = DURATION + 'ms';
          bar.style.transform = 'scaleX(0)';
        });
      }
      startBar();

      const timer = setTimeout(() => dismiss(toast), DURATION);
      function pause() { clearTimeout(timer); bar.style.transitionDuration = '0ms'; }
      function resume() {
        const rect = bar.getBoundingClientRect();
        const remaining = rect.width > 0 ? DURATION * (rect.width / bar.offsetWidth) : DURATION;
        bar.style.transitionDuration = remaining + 'ms';
        bar.style.transform = 'scaleX(0)';
      }

      closeBtn.addEventListener('click', () => { clearTimeout(timer); dismiss(toast); });
      toast.addEventListener('mouseenter', () => pause());
      toast.addEventListener('mouseleave', () => resume());
    }

    window.showToast = showToast;
  })();
</script>
85 lines UTF-8 · LF · Spaces: 2

AI-ready

Available to your agent.

This component is reachable through the DevSnips CLI, MCP server and Skills integrations — one registry, one resource path.

Resource path

devsnips://components/toasts/persistent-toast

Continue browsing

View all