shadcn/ui Sonner-style Toast Notification Library – toast-queue

Category: Javascript , Notification | November 13, 2025
Authorandreruffert
Last UpdateNovember 13, 2025
LicenseMIT
Views154 views
shadcn/ui Sonner-style Toast Notification Library – toast-queue

toast-queue is a headless, accessible, stackable notification library that creates shadcn/ui Sonner-style toast messages for the web.

It provides an elegant stacking system where users can expand all toasts with a click or minimize them back to a compact card view.

Features:

  • Headless architecture: Complete styling control with zero imposed CSS.
  • Intelligent stacking: Visual depth illusion with expand/collapse interactions.
  • Six positioning options: All corners plus top/bottom center placement.
  • Touch-friendly swiping: Native gesture support for dismissing notifications.
  • HTML content support: Rich formatting beyond plain text messages.
  • View Transition API ready: Smooth animations with modern browser features.
  • Accessibility built-in: Proper ARIA labels and keyboard navigation support.

Use Cases:

  • Form validation feedback: Display multiple validation errors in a stackable format that doesn’t overwhelm the interface
  • Background process updates: Show progress notifications for file uploads, data syncing, or batch operations without blocking user workflow
  • Real-time system alerts: Handle multiple concurrent notifications from websocket connections or server-sent events
  • User action confirmations: Provide immediate feedback for save operations, deletions, or settings changes with optional action buttons

How to use it:

1. Import the necessary CSS for basic structure and animations, along with the JavaScript module.

@import url("https://unpkg.com/toast-queue@next/dist/toast-queue.css");
import { ToastQueue } from "https://unpkg.com/toast-queue@next/dist/toast-queue.js";

2. Create a new instance of ToastQueue and pass a configuration object to customize its behavior. We recommend attaching it to the window object for easy access throughout your application.

window.toastQueue = new ToastQueue({
  // 'top start' | 'top center' | 'top end' | 'bottom start' | 'bottom center' | 'bottom end'
  position: 'top-end',      
  // Start with toasts stacked or expanded
  isMinimized: true,        
  // The number of toasts visible in the stack
  maxVisibleToasts: 5,      
  // The DOM element to attach the toast container to
  root: document.body,      
  // Default timeout in ms for all toasts
  timeout: 5000         
      
});

3. Call the add method to display a toast message on the screen. The first argument is the HTML content for the toast, and the second is an optional object for toast-specific settings.

The action property adds an action button to the toast, and you can define a callback function fn that executes when the user clicks it.

window.toastQueue.add(
  `<h1>Update Successful</h1><p>Your profile has been saved.</p>`,
  {
    timeout: 3000, // Override the default timeout
    action: { 
      label: "Undo", 
      fn: () => console.log("Undo action triggered!") 
    }
  }
);

4. API methods and state properties:

// Add a new toast (returns toast reference)
const toast = toastQueue.add(content, options);
// Get toast by ID
const existingToast = toastQueue.get(toastId);
// Remove specific toast
toastQueue.delete(toastId);
// Clear all toasts
toastQueue.clearAll();
// Pause all timers (useful during modal dialogs)
toastQueue.pauseAll();
// Resume all timers
toastQueue.resumeAll();
// Toggle minimized state
toastQueue.isMinimized = false; // Expand all toasts
toastQueue.isMinimized = true;  // Collapse to stack
// Change position dynamically
toastQueue.position = 'bottom end';

6. Since the library is headless, you’ll need to apply your own styles. The elements use data- attributes for easy targeting. Here’s a basic example to get you started.

[data-toast="popover"] {
  --toast-width: 380px;
  padding: 1.5rem;
  text-align: initial;
  gap: 1rem;
  overflow: auto;
  max-height: 100vh;
}
[data-toast="notification"] {
  display: flex;
  justify-content: space-between;
  padding: 1rem;
  background-color: white;
  border: 1px solid #0000001a;
  border-radius: 1rem;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.102);
  position: relative;
  gap: 1rem;
}
[data-toast-button="action"] {
  border: 1px solid #0000001a;
  border-radius: 0.35rem;
  padding: 0.2rem 0.4rem;
}
[data-toast="content"] {
  font-size: 0.85rem;
  text-wrap: balance;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex: 1;
}
[data-toast="content"] h1,
[data-toast="content"] p {
  font-size: inherit;
  margin: unset;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
[data-toast="content"] h1 {
  font-weight: 600;
  -webkit-line-clamp: 1;
}
[data-toast="content"] p {
  -webkit-line-clamp: 3;
}
[data-toast="actions"] {
  display: flex;
  align-items: center;
}
[data-toast="menubar"] {
  view-transition-name: toast-menubar;
  view-transition-class: toast-menubar;
}
::view-transition-old(.toast-actions):only-child {
  --slideX: 0;
  --slideY: calc(-100%);
  animation-name: slide-out;
}
::view-transition-new(.toast-actions):only-child {
  --slideX: 0;
  --slideY: calc(-100%);
  animation-name: slide-in;
}
[data-toast="popover"]:not([data-minimized]) [data-toast="menubar"] {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}
[data-minimized]
  [data-toast="root"]:not(:first-child)
  [data-toast="notification"]
  [data-toast-button="clear"] {
  display: none;
}
[data-toast="notification"] [data-toast-button="clear"] {
  position: absolute;
  right: 100%;
  bottom: 100%;
  translate: 50% 50%;
  aspect-ratio: 1;
  padding: unset;
  width: 1.4rem;
  border-radius: 100%;
  z-index: inherit;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: buttonface;
  border: 1px solid #0000001a;
  font-size: 1rem;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}
[data-toast="notification"]:hover [data-toast-button="clear"],
[data-toast="popover"]:focus-within [data-toast-button="clear"] {
  opacity: 1;
}

7. Handle user interactions through event delegation:

document.addEventListener("click", (event) => {
  // Add new toast
  if (event.target.dataset.action === "add-toast") {
    toastQueue.add("New notification content");
    return;
  }
  // Clear specific toast
  if (event.target.dataset.toastButton === "clear") {
    const toastId = event.target.closest("[data-toast-id]").dataset.toastId;
    toastQueue.delete(toastId);
    return;
  }
  // Execute toast action
  if (event.target.dataset.toastButton === "action") {
    const toastId = event.target.closest("[data-toast-id]").dataset.toastId;
    const toast = toastQueue.get(toastId);
    toast?.action?.fn();
    return;
  }
  // Auto-minimize when clicking outside
  if (!event.target.closest('[data-toast="popover"]')) {
    if (!toastQueue.isMinimized) {
      toastQueue.isMinimized = true;
    }
  }
});

How It Works:

Toast-queue uses a Set-based queue system to manage toast lifecycle and state. Each toast receives a unique random ID and an optional Timer instance that handles auto-dismissal logic.

The core architecture separates concerns between the ToastQueue class that manages state and the Swipeable utility that handles touch gestures. When you add a toast, the library clones HTML templates and injects your content rather than building DOM elements programmatically.

The stacking illusion comes from CSS transforms and View Transition API integration. The library assigns unique view-transition-name properties to each toast, allowing the browser to animate position changes smoothly when toasts are added or removed.

Position management works through CSS custom properties and data attributes. The library calculates swipe directions based on toast positioning – top toasts swipe upward, corner toasts swipe toward their respective edges.

FAQs:

Q: How do I prevent toasts from auto-dismissing when users are interacting with them?
A: The library automatically pauses all timers when users hover over the toast container and resumes them when the pointer leaves. You can also manually call pauseAll() and resumeAll() methods during modal dialogs or other blocking interactions.

Q: What happens if I add more toasts than the maxVisibleToasts limit?
A: Toast-queue shows only the most recent toasts up to your limit. Older toasts remain in memory and become visible again when newer ones are dismissed. This prevents performance issues while preserving all notifications for users who want to review them.

Q: Can I change toast positions dynamically after initialization?
A: Absolutely. Set the position property to any valid position string and the library will update the container positioning and swipe directions automatically. This works well for responsive designs or user preference settings.

Q: Does toast-queue work with server-side rendering?

A: Toast-queue initializes only when the DOM is available and doesn’t require server-side rendering since toast notifications are inherently client-side UI elements. Initialize it after your page loads to avoid hydration issues.

Q: How do I integrate toast-queue with state management libraries like Redux?
A: Store toast data in your state management system and use effects or middleware to call toast-queue methods. Keep the ToastQueue instance as a singleton and trigger add(), delete(), or clearAll() methods based on state changes rather than managing toast state in both places.

Q: How do I create different toast types, like ‘success’ or ‘error’?
A: Because the library is headless, you control the styling. When you call toastQueue.add(), you can include a class or a data attribute in your HTML string. For example: <div class="toast-success">...</div>. Then, you can style that .toast-success class in your CSS to change its colors, icons, etc.

Related Reading:

Changelog:

  • feat: alpha iteration

You Might Be Interested In:


Leave a Reply