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); }); }
};