Component

Promise Toast

Async-operation toast that opens in a loading state and transitions in place to success or error when a promise settles. Pass any promise to showPromiseToast() with loading/success/error copy; once settled it auto-dismisses behind a pause-on-hover countdown bar.

  • Component
  • Tailwind
  • HTML
  • MIT

Preview

Live component

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

Install

Add to your project

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

Source

Implementation

code.html
<!--
Snippet Name: Promise Toast
Description: Async-operation toast that opens in a loading state and transitions in place to success or error when a promise settles, with a pause-on-hover countdown bar.
Author: DevSnips Contributors
Usage Example: showPromiseToast(saveSettings(), { loading: { title: 'Saving…' }, success: { title: 'Settings saved' }, error: { title: 'Save failed' } })
-->
<div data-promise-toast="promise-toast">
  <!-- Toast region (fixed to the viewport corner) -->
  <div
    data-toast-region
    role="region"
    aria-label="Notifications"
    aria-live="polite"
    class="pointer-events-none fixed bottom-4 left-4 right-4 z-50 flex flex-col items-stretch gap-3 sm:left-auto sm:right-4 sm:w-full sm:max-w-sm sm:items-end"
  ></div>

  <!-- Demo triggers -->
  <div class="flex flex-wrap items-center gap-4">
    <button
      type="button"
      data-demo="success"
      class="inline-flex min-h-[44px] items-center gap-2 border-2 border-black bg-[#00C2FF] px-5 py-3 text-sm font-extrabold tracking-tight text-black shadow-[6px_6px_0_0_#000] motion-safe:transition-all motion-safe:duration-150 hover:translate-x-[3px] hover:translate-y-[3px] hover:shadow-[3px_3px_0_0_#000] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2 active:translate-x-[6px] active:translate-y-[6px] active:shadow-none"
    >
      <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 15V4m0 0L8 8m4-4 4 4M4 20h16" stroke="currentColor" stroke-width="2.5" stroke-linecap="square"/></svg>
      Deploy to production
    </button>
    <button
      type="button"
      data-demo="error"
      class="inline-flex min-h-[44px] items-center gap-2 border-2 border-black bg-white px-5 py-3 text-sm font-extrabold tracking-tight text-black shadow-[6px_6px_0_0_#000] motion-safe:transition-all motion-safe:duration-150 hover:translate-x-[3px] hover:translate-y-[3px] hover:bg-[#FFE600] hover:shadow-[3px_3px_0_0_#000] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2 active:translate-x-[6px] active:translate-y-[6px] active:shadow-none"
    >
      <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 9v4m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="round"/></svg>
      Simulate failed deploy
    </button>
  </div>

  <script>
    (function () {
      var root = document.currentScript.closest('[data-promise-toast="promise-toast"]');
      if (!root) return;
      var region = root.querySelector('[data-toast-region]');
      var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var DEFAULT_DURATION = 5000;

      var ICONS = {
        loading: '<svg class="h-4 w-4 motion-safe:animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 3a9 9 0 1 0 9 9" stroke="currentColor" stroke-width="3.5" stroke-linecap="square"/></svg>',
        success: '<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5" stroke="currentColor" stroke-width="3.5" stroke-linecap="square"/></svg>',
        error: '<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" stroke="currentColor" stroke-width="3.5" stroke-linecap="square"/></svg>'
      };
      var CHIP_BG = { loading: 'bg-[#FFE600]', success: 'bg-[#00E676]', error: 'bg-[#FF4FA3]' };
      var ROLE = { loading: 'status', success: 'status', error: 'alert' };

      function dismiss(toast) {
        if (!toast || toast.dataset.dismissed === 'true') return;
        toast.dataset.dismissed = 'true';
        toast.classList.add('translate-y-2', 'opacity-0');
        setTimeout(function () { toast.remove(); }, 220);
      }

      function buildToast(copy, state) {
        var toast = document.createElement('div');
        toast.className = 'pointer-events-auto relative w-full translate-y-3 overflow-hidden border-2 border-black bg-white p-4 opacity-0 shadow-[6px_6px_0_0_#000] motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-out';
        toast.setAttribute('role', ROLE[state]);
        toast.innerHTML =
          '<div class="flex items-start gap-3">' +
            '<span data-chip class="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center border-2 border-black text-black motion-safe:transition-colors motion-safe:duration-200 ' + CHIP_BG[state] + '">' + ICONS[state] + '</span>' +
            '<div class="min-w-0 flex-1 pt-0.5">' +
              '<p data-title class="text-sm font-extrabold tracking-tight text-black"></p>' +
              '<p data-desc class="mt-0.5 text-sm leading-snug text-black/60"></p>' +
            '</div>' +
            '<button type="button" data-close aria-label="Dismiss notification" class="-mr-2 -mt-2 flex h-11 w-11 shrink-0 items-center justify-center text-black/50 motion-safe:transition-colors motion-safe:duration-150 hover:bg-[#FFE600] hover:text-black focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-black">' +
              '<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" stroke="currentColor" stroke-width="2.5" stroke-linecap="square"/></svg>' +
            '</button>' +
          '</div>' +
          '<span data-bar hidden class="absolute bottom-0 left-0 h-1 w-full bg-black"></span>';
        toast.querySelector('[data-title]').textContent = copy.title || '';
        toast.querySelector('[data-desc]').textContent = copy.description || '';
        toast.querySelector('[data-close]').addEventListener('click', function () { dismiss(toast); });
        toast.addEventListener('keydown', function (event) {
          if (event.key === 'Escape') dismiss(toast);
        });
        return toast;
      }

      function setState(toast, state, copy) {
        var chip = toast.querySelector('[data-chip]');
        chip.classList.remove(CHIP_BG.loading, CHIP_BG.success, CHIP_BG.error);
        chip.classList.add(CHIP_BG[state]);
        chip.innerHTML = ICONS[state];
        toast.querySelector('[data-title]').textContent = copy.title || '';
        toast.querySelector('[data-desc]').textContent = copy.description || '';
        toast.setAttribute('role', ROLE[state]);
      }

      // Auto-dismiss countdown, mirrored on the progress bar and paused
      // while the toast is hovered or holds keyboard focus.
      function armCountdown(toast, duration) {
        var bar = toast.querySelector('[data-bar]');
        var remaining = duration;
        var startedAt = 0;
        var timer = null;

        function start() {
          startedAt = Date.now();
          if (bar && !reduceMotion) {
            bar.style.transitionProperty = 'width';
            bar.style.transitionTimingFunction = 'linear';
            bar.style.transitionDuration = remaining + 'ms';
            requestAnimationFrame(function () { bar.style.width = '0%'; });
          }
          timer = setTimeout(function () { dismiss(toast); }, remaining);
        }

        function pause() {
          if (timer === null) return;
          clearTimeout(timer);
          timer = null;
          remaining -= Date.now() - startedAt;
          if (bar && !reduceMotion) {
            bar.style.transitionDuration = '0ms';
            bar.style.width = window.getComputedStyle(bar).width;
          }
        }

        function resume() {
          if (timer !== null || remaining <= 0) return;
          start();
        }

        if (bar) bar.hidden = false;
        toast.addEventListener('mouseenter', pause);
        toast.addEventListener('mouseleave', resume);
        toast.addEventListener('focusin', pause);
        toast.addEventListener('focusout', resume);
        start();
      }

      function showPromiseToast(promise, messages, opts) {
        opts = opts || {};
        messages = messages || {};
        var duration = opts.duration || DEFAULT_DURATION;
        var toast = buildToast(messages.loading || {}, 'loading');
        region.appendChild(toast);
        requestAnimationFrame(function () {
          toast.classList.remove('translate-y-3', 'opacity-0');
        });

        function settle(state) {
          if (toast.dataset.dismissed === 'true') return;
          setState(toast, state, messages[state] || {});
          armCountdown(toast, duration);
        }

        Promise.resolve(promise).then(
          function () { settle('success'); },
          function () { settle('error'); }
        );
        return toast;
      }

      window.showPromiseToast = showPromiseToast;

      // Demo wiring: simulated deploys
      function fakeDeploy(shouldFail) {
        return new Promise(function (resolve, reject) {
          setTimeout(function () {
            if (shouldFail) reject(new Error('Build timed out'));
            else resolve();
          }, 2400);
        });
      }

      root.querySelector('[data-demo="success"]').addEventListener('click', function () {
        showPromiseToast(fakeDeploy(false), {
          loading: { title: 'Deploying storefront…', description: 'Build #248 is being promoted to production.' },
          success: { title: 'Deployment live', description: 'Build #248 is now serving production traffic.' },
          error: { title: 'Deployment failed', description: 'Something went wrong while promoting build #248.' }
        });
      });
      root.querySelector('[data-demo="error"]').addEventListener('click', function () {
        showPromiseToast(fakeDeploy(true), {
          loading: { title: 'Deploying payments-api…', description: 'Build #96 is being promoted to production.' },
          success: { title: 'Deployment live', description: 'Build #96 is now serving production traffic.' },
          error: { title: 'Deployment failed', description: 'Build #96 timed out after 90s. Check the build log.' }
        });
      });
    })();
  </script>
</div>
189 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/promise-toast

Continue browsing

View all