Toast
Transient notification using popover="manual". Imperative toast.show() API
with auto-dismiss, stacking, destructive variant, and action buttons.
Default
Basic toast with title and description via toast.show().
<button class="btn" data-variant="default"
onclick="toast.show({ title: 'Event created', description: 'Monday, January 3rd at 6:00pm' })">
Show Toast
</button>
<script>
toast.show({
title: 'Event created',
description: 'Monday, January 3rd at 6:00pm'
});
</script>
Variants
Semantic variants add a colored border and icon. Use toast.success(), toast.warning(), toast.info(), or toast.error().
<button class="btn" data-variant="outline"
onclick="toast.success({ title: 'Success', description: 'Your changes have been saved.' })">
Success
</button>
<button class="btn" data-variant="outline"
onclick="toast.warning({ title: 'Warning', description: 'Your session will expire in 5 minutes.' })">
Warning
</button>
<button class="btn" data-variant="outline"
onclick="toast.info({ title: 'Info', description: 'A new version is available for download.' })">
Info
</button>
<button class="btn" data-variant="destructive"
onclick="toast.error({ title: 'Error', description: 'Something went wrong.' })">
Error
</button>
<script>
toast.success({ title: 'Success', description: 'Your changes have been saved.' });
toast.warning({ title: 'Warning', description: 'Your session will expire in 5 minutes.' });
toast.info({ title: 'Info', description: 'A new version is available for download.' });
toast.error({ title: 'Error', description: 'Something went wrong.' });
</script>
With Description
Pass a description for a secondary line of detail below the title.
<button class="btn" data-variant="outline"
onclick="toast.show({ title: 'Scheduled', description: 'Your report will be generated at 9:00 AM tomorrow.' })">
Show Toast
</button>
<script>
toast.show({
title: 'Scheduled',
description: 'Your report will be generated at 9:00 AM tomorrow.'
});
</script>
With Action
Add an action object with label and onClick to render an action button inside the toast.
<button class="btn" data-variant="outline"
onclick="toast.show({ title: 'Changes saved', description: 'Your profile has been updated.', action: { label: 'Undo', onClick: () => toast.show('Undone!') } })">
Show Toast
</button>
<script>
toast.show({
title: 'Changes saved',
description: 'Your profile has been updated.',
action: {
label: 'Undo',
onClick: () => toast.show('Undone!')
}
});
</script>
CSS view file
/* -- Toast component ------------------------------------------- */
@layer components {
/* -- Container ------------------------------------------------ */
.toast-container {
position: fixed;
z-index: 100;
display: flex;
flex-direction: column-reverse;
gap: 0.5rem;
padding: 1rem;
max-height: 100vh;
pointer-events: none;
&[data-position="bottom-right"] { bottom: 0; right: 0; }
&[data-position="bottom-left"] { bottom: 0; left: 0; }
&[data-position="top-right"] { top: 0; right: 0; flex-direction: column; }
&[data-position="top-left"] { top: 0; left: 0; flex-direction: column; }
&[data-position="top-center"] { top: 0; left: 50%; transform: translateX(-50%); flex-direction: column; align-items: center; }
&[data-position="bottom-center"]{ bottom: 0; left: 50%; transform: translateX(-50%); align-items: center; }
}
/* -- Toast ---------------------------------------------------- */
.toast {
background-color: var(--popover);
color: var(--popover-foreground);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 1rem;
min-width: 20rem;
max-width: 26rem;
box-shadow: 0 4px 16px oklch(0 0 0 / 0.12);
pointer-events: auto;
opacity: 0;
transform: translateY(0.5rem);
transition: opacity 200ms ease, transform 200ms ease;
&:popover-open {
opacity: 1;
transform: translateY(0);
}
/* -- Variants ------------------------------------------------- */
&[data-variant="destructive"] {
background-color: var(--destructive);
color: var(--destructive-foreground);
border-color: var(--destructive);
& .toast-description {
color: var(--destructive-foreground);
opacity: 0.85;
}
}
/* -- Semantic variants (border + icon color) ----------------- */
&[data-variant="success"] {
border-color: oklch(0.65 0.2 145);
& .toast-icon { color: oklch(0.65 0.2 145); }
}
&[data-variant="warning"] {
border-color: oklch(0.75 0.18 75);
& .toast-icon { color: oklch(0.75 0.18 75); }
}
&[data-variant="info"] {
border-color: oklch(0.6 0.15 250);
& .toast-icon { color: oklch(0.6 0.15 250); }
}
}
@starting-style {
.toast:popover-open {
opacity: 0;
transform: translateY(0.5rem);
}
}
/* -- Inner elements ------------------------------------------- */
.toast-content {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.toast-text {
flex: 1;
min-width: 0;
}
.toast-title {
font-size: 0.875rem;
font-weight: 500;
margin: 0;
line-height: 1.4;
}
.toast-description {
font-size: 0.8125rem;
color: var(--muted-foreground);
margin: 0.125rem 0 0;
line-height: 1.5;
}
.toast-close {
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
border: none;
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
flex-shrink: 0;
border-radius: calc(var(--radius) * 0.5);
transition: color 150ms, background 150ms;
&:hover {
color: var(--foreground);
background: var(--accent);
}
}
.toast-icon {
flex-shrink: 0;
margin-top: 0.125rem;
}
.toast-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
}
}
JavaScript view file
Imperative toast.show() API with auto-dismiss, stacking (max 3 visible), variant icons, and action buttons. Container is auto-created if not present in the DOM.
// -- Toast -----------------------------------------------------
// Programmatic toast notification API.
// Exposes window.toast with show/success/warning/info/error/dismiss.
const DURATION = 4000;
const MAX_VISIBLE = 3;
let toastContainer = document.getElementById('toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.className = 'toast-container';
toastContainer.setAttribute('aria-label', 'Notifications');
toastContainer.setAttribute('data-position', 'bottom-right');
document.body.appendChild(toastContainer);
}
const toastDismiss = (el, callback) => {
if (!el || !el.parentNode) return;
el.animate(
[{ opacity: 1, transform: 'translateY(0)' }, { opacity: 0, transform: 'translateY(0.5rem)' }],
{ duration: 200, easing: 'ease', fill: 'forwards' }
).finished.then(() => { try { el.hidePopover(); } catch(e) {} el.remove(); if (callback) callback(); });
};
const toastCreate = (options) => {
const o = typeof options === 'string' ? { title: options } : options;
const { title, description, variant, action, onDismiss } = o;
const duration = o.duration != null ? o.duration : DURATION;
const el = document.createElement('div'); el.className = 'toast';
el.setAttribute('role', variant === 'destructive' ? 'alert' : 'status');
el.setAttribute('aria-live', variant === 'destructive' ? 'assertive' : 'polite');
el.setAttribute('aria-atomic', 'true'); el.setAttribute('popover', 'manual');
if (variant) el.setAttribute('data-variant', variant);
const icons = {
success: '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
warning: '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>',
info: '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
destructive: '<svg class="toast-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>'
};
// Build toast DOM safely (no innerHTML with user content)
const contentEl = document.createElement('div');
contentEl.className = 'toast-content';
if (variant && icons[variant]) {
const tmpl = document.createElement('template');
tmpl.innerHTML = icons[variant];
contentEl.appendChild(tmpl.content);
}
const textDiv = document.createElement('div');
textDiv.className = 'toast-text';
if (title) { const p = document.createElement('p'); p.className = 'toast-title'; p.textContent = title; textDiv.appendChild(p); }
if (description) { const p = document.createElement('p'); p.className = 'toast-description'; p.textContent = description; textDiv.appendChild(p); }
contentEl.appendChild(textDiv);
const closeBtn = document.createElement('button');
closeBtn.className = 'toast-close'; closeBtn.setAttribute('aria-label', 'Dismiss'); closeBtn.dataset.toastClose = '';
closeBtn.innerHTML = '<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>';
contentEl.appendChild(closeBtn);
el.appendChild(contentEl);
if (action) {
const actionsDiv = document.createElement('div'); actionsDiv.className = 'toast-actions';
const actionBtn = document.createElement('button'); actionBtn.className = 'btn';
actionBtn.setAttribute('data-variant', 'outline'); actionBtn.setAttribute('data-size', 'sm'); actionBtn.dataset.toastAction = '';
actionBtn.textContent = action.label;
actionsDiv.appendChild(actionBtn); el.appendChild(actionsDiv);
}
toastContainer.appendChild(el); el.showPopover();
closeBtn.addEventListener('click', () => { toastDismiss(el, onDismiss); });
if (action) { el.querySelector('[data-toast-action]').addEventListener('click', () => { if (action.onClick) action.onClick(); toastDismiss(el); }); }
if (duration !== Infinity) setTimeout(() => { toastDismiss(el, onDismiss); }, duration);
const toasts = toastContainer.querySelectorAll('.toast');
if (toasts.length > MAX_VISIBLE) toastDismiss(toasts[0]);
return el;
};
window.toast = {
show: toastCreate,
success: (o) => toastCreate(Object.assign(typeof o === 'string' ? { title: o } : o, { variant: 'success' })),
warning: (o) => toastCreate(Object.assign(typeof o === 'string' ? { title: o } : o, { variant: 'warning' })),
info: (o) => toastCreate(Object.assign(typeof o === 'string' ? { title: o } : o, { variant: 'info' })),
error: (o) => toastCreate(Object.assign(typeof o === 'string' ? { title: o } : o, { variant: 'destructive' })),
dismiss: () => { toastContainer.querySelectorAll('.toast').forEach((el) => { toastDismiss(el); }); }
};