Sonner

Overlay & Feedback

Stacked toast notification system.

Preview

Stacked toast notification system. Wrap your app in <SonnerProvider> and use useSonner() to trigger toasts.

Usage

example.jsx
import { SonnerProvider } from "@/components/ui/sonner";

export default function Example() {
  return <SonnerProvider />;
}

Source Code

Copy this file into components/ui/sonner.jsx in your project.

sonner.jsx
"use client";

import { createContext, useContext, useState, useCallback } from "react";
import { cn } from "@/lib/utils";

const SonnerContext = createContext(null);

export function SonnerProvider({ children, position = "bottom-right" }) {
  const [toasts, setToasts] = useState([]);

  const toast = useCallback((message, opts = {}) => {
    const id = Date.now();
    setToasts((p) => [...p, { id, message, type: opts.type || "default", description: opts.description }]);
    setTimeout(() => setToasts((p) => p.filter((t) => t.id !== id)), opts.duration || 4000);
  }, []);

  const posClass = {
    "bottom-right": "bottom-4 right-4",
    "bottom-left": "bottom-4 left-4",
    "top-right": "top-4 right-4",
    "top-left": "top-4 left-4",
    "top-center": "top-4 left-1/2 -translate-x-1/2",
    "bottom-center": "bottom-4 left-1/2 -translate-x-1/2",
  }[position];

  return (
    <SonnerContext.Provider value={toast}>
      {children}
      <div className={cn("fixed z-[100] flex flex-col gap-2", posClass)}>
        {toasts.map((t) => (
          <div key={t.id} className={cn("flex items-start gap-3 rounded-lg border bg-background px-4 py-3 shadow-lg animate-in slide-in-from-bottom-2",
            t.type === "success" && "border-green-500/50 bg-green-50 dark:bg-green-950",
            t.type === "error" && "border-red-500/50 bg-red-50 dark:bg-red-950",
            t.type === "warning" && "border-yellow-500/50 bg-yellow-50 dark:bg-yellow-950"
          )}>
            {t.type === "success" && <span className="text-green-500">✓</span>}
            {t.type === "error" && <span className="text-red-500">✕</span>}
            {t.type === "warning" && <span className="text-yellow-500">⚠</span>}
            <div>
              <div className="text-sm font-medium">{t.message}</div>
              {t.description && <div className="text-xs text-muted-foreground">{t.description}</div>}
            </div>
          </div>
        ))}
      </div>
    </SonnerContext.Provider>
  );
}

export function useSonner() {
  const toast = useContext(SonnerContext);
  return {
    toast,
    success: (msg, opts) => toast?.(msg, { ...opts, type: "success" }),
    error: (msg, opts) => toast?.(msg, { ...opts, type: "error" }),
    warning: (msg, opts) => toast?.(msg, { ...opts, type: "warning" }),
  };
}

Quick Install

Make sure you have the cn() utility set up. It requires clsx and tailwind-merge.

npm install clsx tailwind-merge