Skip to main content
Back to Blog
Tutorials
2 min read
November 9, 2024

How to Build a Toast Notification System in React

Add a toast notification system to your React app with sonner. Success, error, loading, and custom toast types.

Ryel Banfield

Founder & Lead Developer

Toast notifications give users feedback without interrupting their workflow. The sonner library provides a clean, accessible toast system.

Step 1: Install Sonner

pnpm add sonner

Step 2: Add the Toaster Provider

// app/layout.tsx
import { Toaster } from "sonner";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster
          position="bottom-right"
          richColors
          toastOptions={{
            className: "font-sans",
          }}
        />
      </body>
    </html>
  );
}

Step 3: Basic Toast Usage

"use client";

import { toast } from "sonner";

export function ActionButtons() {
  return (
    <div className="flex gap-3">
      <button onClick={() => toast("Event has been created")}>
        Default
      </button>
      <button onClick={() => toast.success("Successfully saved!")}>
        Success
      </button>
      <button onClick={() => toast.error("Something went wrong")}>
        Error
      </button>
      <button onClick={() => toast.warning("Are you sure?")}>
        Warning
      </button>
      <button onClick={() => toast.info("New feature available")}>
        Info
      </button>
    </div>
  );
}

Step 4: Toast with Description

toast.success("Profile updated", {
  description: "Your changes have been saved successfully.",
});

Step 5: Toast with Action Button

toast("File deleted", {
  action: {
    label: "Undo",
    onClick: () => restoreFile(fileId),
  },
});

Step 6: Promise Toast (Loading States)

async function handleSave(data: FormData) {
  toast.promise(saveToDatabase(data), {
    loading: "Saving changes...",
    success: "Changes saved!",
    error: "Could not save. Please try again.",
  });
}

// With async/await
async function handleSubmit() {
  const id = toast.loading("Submitting form...");

  try {
    await submitForm();
    toast.success("Form submitted!", { id });
  } catch {
    toast.error("Submission failed", { id });
  }
}

Step 7: Custom Toast Component

toast.custom((id) => (
  <div className="flex items-center gap-3 rounded-xl border bg-white p-4 shadow-lg dark:border-gray-700 dark:bg-gray-800">
    <img
      src="/avatar.jpg"
      alt="User"
      className="h-10 w-10 rounded-full object-cover"
    />
    <div className="flex-1">
      <p className="text-sm font-medium">New message from Alex</p>
      <p className="text-xs text-gray-500">Hey, the project looks great!</p>
    </div>
    <button
      onClick={() => toast.dismiss(id)}
      className="text-xs text-gray-400 hover:text-gray-600"
    >
      Dismiss
    </button>
  </div>
));

Step 8: Configuration Options

<Toaster
  position="bottom-right"    // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right
  richColors                 // Colorful success/error styles
  expand={false}             // Expand toasts by default
  duration={4000}            // Default duration in ms
  closeButton                // Show close button
  theme="system"             // light, dark, system
  visibleToasts={3}          // Max visible at once
/>

Step 9: Toast on Form Submission with Server Actions

"use client";

import { toast } from "sonner";
import { useTransition } from "react";
import { submitContactForm } from "@/app/actions";

export function ContactForm() {
  const [isPending, startTransition] = useTransition();

  function handleSubmit(formData: FormData) {
    startTransition(async () => {
      const result = await submitContactForm(formData);

      if (result.success) {
        toast.success("Message sent!", {
          description: "We will get back to you within 24 hours.",
        });
      } else {
        toast.error("Failed to send message", {
          description: result.error,
        });
      }
    });
  }

  return (
    <form action={handleSubmit}>
      {/* form fields */}
      <button type="submit" disabled={isPending}>
        {isPending ? "Sending..." : "Send Message"}
      </button>
    </form>
  );
}

Step 10: Toast Utility Wrapper

// lib/toast.ts
import { toast } from "sonner";

export const notify = {
  success: (message: string, description?: string) =>
    toast.success(message, { description }),

  error: (message: string, description?: string) =>
    toast.error(message, { description }),

  promise: <T>(
    promise: Promise<T>,
    messages: { loading: string; success: string; error: string }
  ) => toast.promise(promise, messages),

  clipboard: (text: string) => {
    navigator.clipboard.writeText(text);
    toast.success("Copied to clipboard");
  },
};

Need Polished UI Components?

We build web applications with professional UI patterns including toast notifications, modals, and interactive feedback systems. Contact us to start your project.

toast notificationsReactsonnertutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles