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
9:41 100%
devsnips.dev/library/tailwind/components/toasts/promise-toast/ fluid · no fixed width 100%
Install
Add to your project
npx devsnips add Tailwind/Components/Toasts/promise-toast Tailwind/Components/Toasts/promise-toast <!--
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> # Promise Toast
Toast for async/promise states (loading → success/error).
## Usage
```bash
npx devsnips add Tailwind/Components/Toasts/promise-toast
```
Copy from `code.html`.
## File Structure
`code.html` · `preview.html` · `metadata.json` · `README.md` 189 lines UTF-8 · LF · Spaces: 2
Continue browsing