DEV Community

James Parker
James Parker

Posted on

Advanced Toast Notifications with Sonner in React

Sonner is an opinionated toast component library for React that provides beautiful, customizable toast notifications with a focus on developer experience and performance. It offers advanced features like promise handling, custom JSX content, rich styling options, and excellent TypeScript support. This guide walks through advanced usage of Sonner with React, including custom configurations, promise handling, and complex notification patterns. This is part 39 of a series on using Sonner with React.

Prerequisites

Before you begin, make sure you have:

  • Node.js version 14.0 or higher installed
  • npm, yarn, or pnpm package manager
  • A React project (version 16.8 or higher) or create-react-app setup
  • Basic knowledge of React hooks (useState, useCallback)
  • Familiarity with async/await and Promises
  • Understanding of TypeScript (optional but recommended)
  • Understanding of CSS for styling

Installation

Install Sonner using your preferred package manager:

npm install sonner
Enter fullscreen mode Exit fullscreen mode

Or with yarn:

yarn add sonner
Enter fullscreen mode Exit fullscreen mode

Or with pnpm:

pnpm add sonner
Enter fullscreen mode Exit fullscreen mode

After installation, your package.json should include:

{
  "dependencies": {
    "sonner": "^1.0.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Project Setup

Set up the Toaster component in your main entry file or App component:

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Toaster } from 'sonner';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
    <Toaster />
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Or add it to your App component:

// src/App.jsx
import React from 'react';
import { Toaster } from 'sonner';
import './App.css';

function App() {
  return (
    <div className="App">
      <Toaster />
      {/* Your app content */}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

First Example / Basic Usage

Let's create a simple toast notification component. Create a new file src/ToastExample.jsx:

// src/ToastExample.jsx
import React from 'react';
import { toast } from 'sonner';

function ToastExample() {
  const showSuccess = () => {
    toast.success('Operation completed successfully!');
  };

  const showError = () => {
    toast.error('Something went wrong!');
  };

  const showInfo = () => {
    toast.info('Here is some information.');
  };

  const showWarning = () => {
    toast.warning('Please review your input.');
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Toast Notification Examples</h2>
      <div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
        <button onClick={showSuccess}>Success</button>
        <button onClick={showError}>Error</button>
        <button onClick={showInfo}>Info</button>
        <button onClick={showWarning}>Warning</button>
      </div>
    </div>
  );
}

export default ToastExample;
Enter fullscreen mode Exit fullscreen mode

Update your App.jsx:

// src/App.jsx
import React from 'react';
import { Toaster } from 'sonner';
import ToastExample from './ToastExample';
import './App.css';

function App() {
  return (
    <div className="App">
      <ToastExample />
      <Toaster />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Understanding the Basics

Sonner provides several key features:

  • toast.success: Show a success toast
  • toast.error: Show an error toast
  • toast.info: Show an info toast
  • toast.warning: Show a warning toast
  • toast.promise: Handle promises with automatic loading/success/error states
  • toast.custom: Show custom JSX content
  • Toaster: Component that renders all toasts
  • Rich Colors: Automatic color theming for different toast types
  • TypeScript: Full TypeScript support

Key concepts for advanced usage:

  • Promise Handling: Use toast.promise for async operations
  • Custom JSX: Pass React components as toast content
  • Styling: Extensive styling options with inline styles and class names
  • Global Configuration: Configure default options on the Toaster component
  • Action Buttons: Add action buttons to toasts

Here's an example with promise handling and custom content:

// src/AdvancedToastExample.jsx
import React from 'react';
import { toast } from 'sonner';

function AdvancedToastExample() {
  const showPromiseToast = () => {
    const fetchData = () => 
      fetch('/api/data')
        .then(response => {
          if (!response.ok) throw new Error('Network error');
          return response.json();
        });

    toast.promise(fetchData, {
      loading: 'Loading data...',
      success: (data) => ({
        title: 'Data loaded!',
        description: `Loaded ${data.items?.length || 0} items`,
        duration: 5000,
      }),
      error: (error) => ({
        title: 'Failed to load data',
        description: error.message,
      }),
    });
  };

  const showCustomJSX = () => {
    toast.custom((t) => (
      <div
        style={{
          background: 'white',
          padding: '16px',
          borderRadius: '8px',
          boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
          opacity: t.visible ? 1 : 0,
          transition: 'opacity 0.3s',
        }}
      >
        <strong>Custom JSX Toast</strong>
        <p>This is a custom toast with JSX content.</p>
        <button onClick={() => toast.dismiss(t.id)}>Close</button>
      </div>
    ));
  };

  const showStyledToast = () => {
    toast('Custom styled toast', {
      style: {
        background: '#1e40af',
        color: 'white',
        border: '2px solid #3b82f6',
      },
      className: 'my-custom-toast',
    });
  };

  return (
    <div style={{ padding: '20px' }}>
      <h2>Advanced Toast Examples</h2>
      <div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
        <button onClick={showPromiseToast}>Promise Toast</button>
        <button onClick={showCustomJSX}>Custom JSX</button>
        <button onClick={showStyledToast}>Styled Toast</button>
      </div>
    </div>
  );
}

export default AdvancedToastExample;
Enter fullscreen mode Exit fullscreen mode

Practical Example / Building Something Real

Let's build a comprehensive notification system with custom hooks, promise handling, and advanced features:

// src/hooks/useToastNotifications.js
import { useCallback } from 'react';
import { toast } from 'sonner';

export const useToastNotifications = () => {
  const showSuccess = useCallback((title, description, options = {}) => {
    toast.success(title, {
      description,
      duration: 3000,
      ...options
    });
  }, []);

  const showError = useCallback((title, description, options = {}) => {
    toast.error(title, {
      description,
      duration: 5000,
      ...options
    });
  }, []);

  const showPromise = useCallback((promise, messages) => {
    return toast.promise(promise, {
      loading: messages.loading || 'Loading...',
      success: messages.success || 'Success!',
      error: messages.error || 'Error occurred',
      ...messages
    });
  }, []);

  const showCustom = useCallback((content, options = {}) => {
    return toast.custom(content, options);
  }, []);

  const dismiss = useCallback((toastId) => {
    toast.dismiss(toastId);
  }, []);

  return {
    showSuccess,
    showError,
    showPromise,
    showCustom,
    dismiss
  };
};
Enter fullscreen mode Exit fullscreen mode

Now create an advanced notification system component:

// src/AdvancedToastSystem.jsx
import React, { useState } from 'react';
import { useToastNotifications } from './hooks/useToastNotifications';
import { toast } from 'sonner';

function AdvancedToastSystem() {
  const { showSuccess, showError, showPromise, showCustom, dismiss } = useToastNotifications();
  const [formData, setFormData] = useState({
    title: '',
    description: '',
    type: 'success'
  });

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!formData.title) {
      showError('Validation Error', 'Please enter a title.');
      return;
    }

    switch (formData.type) {
      case 'success':
        showSuccess(formData.title, formData.description);
        break;
      case 'error':
        showError(formData.title, formData.description);
        break;
      default:
        showSuccess(formData.title, formData.description);
    }

    setFormData({ title: '', description: '', type: 'success' });
  };

  const handlePromiseOperation = () => {
    const fetchUser = () => 
      fetch('https://api.example.com/user')
        .then(response => {
          if (!response.ok) throw new Error('Network error');
          return response.json();
        });

    showPromise(fetchUser, {
      loading: 'Loading user...',
      success: (data) => ({
        title: `Welcome ${data.name}!`,
        description: data.email,
        duration: 5000,
      }),
      error: (error) => ({
        title: 'Failed to load user',
        description: error.message,
      }),
    });
  };

  const showActionToast = () => {
    toast('Item will be deleted', {
      action: {
        label: 'Undo',
        onClick: () => {
          showSuccess('Action cancelled', 'The item was not deleted.');
        },
      },
      cancel: {
        label: 'Cancel',
        onClick: () => {
          toast.dismiss();
        },
      },
    });
  };

  const showCustomContent = () => {
    showCustom(
      (t) => (
        <div
          style={{
            background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
            color: 'white',
            padding: '20px',
            borderRadius: '12px',
            boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
            opacity: t.visible ? 1 : 0,
            transition: 'opacity 0.3s',
            minWidth: '300px',
          }}
        >
          <h3 style={{ margin: '0 0 8px 0' }}>Custom Toast</h3>
          <p style={{ margin: 0, opacity: 0.9 }}>
            This is a completely custom toast with gradient background.
          </p>
          <button
            onClick={() => dismiss(t.id)}
            style={{
              marginTop: '12px',
              padding: '6px 12px',
              background: 'rgba(255, 255, 255, 0.2)',
              border: '1px solid rgba(255, 255, 255, 0.3)',
              borderRadius: '6px',
              color: 'white',
              cursor: 'pointer',
            }}
          >
            Close
          </button>
        </div>
      ),
      {
        duration: Infinity,
      }
    );
  };

  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <h1>Advanced Toast System</h1>

      <form onSubmit={handleSubmit} style={{ marginBottom: '20px', padding: '20px', border: '1px solid #ddd', borderRadius: '8px' }}>
        <div style={{ marginBottom: '16px' }}>
          <label style={{ display: 'block', marginBottom: '4px', fontWeight: '500' }}>
            Title *
          </label>
          <input
            type="text"
            name="title"
            value={formData.title}
            onChange={handleInputChange}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid #ddd',
              borderRadius: '4px',
              boxSizing: 'border-box'
            }}
            required
          />
        </div>

        <div style={{ marginBottom: '16px' }}>
          <label style={{ display: 'block', marginBottom: '4px', fontWeight: '500' }}>
            Description
          </label>
          <textarea
            name="description"
            value={formData.description}
            onChange={handleInputChange}
            rows={2}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid #ddd',
              borderRadius: '4px',
              boxSizing: 'border-box',
              resize: 'vertical'
            }}
          />
        </div>

        <div style={{ marginBottom: '16px' }}>
          <label style={{ display: 'block', marginBottom: '4px', fontWeight: '500' }}>
            Type
          </label>
          <select
            name="type"
            value={formData.type}
            onChange={handleInputChange}
            style={{
              width: '100%',
              padding: '8px',
              border: '1px solid #ddd',
              borderRadius: '4px',
              boxSizing: 'border-box'
            }}
          >
            <option value="success">Success</option>
            <option value="error">Error</option>
          </select>
        </div>

        <button
          type="submit"
          style={{
            width: '100%',
            padding: '10px',
            backgroundColor: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '16px'
          }}
        >
          Show Toast
        </button>
      </form>

      <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
        <button
          onClick={handlePromiseOperation}
          style={{
            padding: '10px',
            backgroundColor: '#28a745',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          Promise Operation
        </button>
        <button
          onClick={showActionToast}
          style={{
            padding: '10px',
            backgroundColor: '#ffc107',
            color: 'black',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          Action Toast
        </button>
        <button
          onClick={showCustomContent}
          style={{
            padding: '10px',
            backgroundColor: '#6c757d',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
          }}
        >
          Custom Content Toast
        </button>
      </div>
    </div>
  );
}

export default AdvancedToastSystem;
Enter fullscreen mode Exit fullscreen mode

Update your App.jsx with custom Toaster configuration:

// src/App.jsx
import React from 'react';
import { Toaster } from 'sonner';
import AdvancedToastSystem from './AdvancedToastSystem';
import './App.css';

function App() {
  return (
    <div className="App">
      <AdvancedToastSystem />
      <Toaster
        position="top-right"
        duration={4000}
        visibleToasts={5}
        expand={true}
        richColors={true}
        closeButton={true}
        toastOptions={{
          className: 'my-toast',
          style: {
            background: 'white',
            color: 'black',
          },
        }}
      />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Common Issues / Troubleshooting

  1. Toasts not displaying: Ensure Toaster component is rendered in your app. It should be placed at the root level, typically in your main App component or index file.

  2. Promise toasts not working: Make sure the promise is properly structured. toast.promise automatically handles loading, success, and error states based on the promise outcome. The promise can be a function that returns a promise or a promise directly.

  3. Custom JSX not rendering: When using toast.custom, the function receives a toast object with properties like id and visible. Use these to control the toast's visibility and dismissal.

  4. Styling not applying: Use the style prop for inline styles or className for CSS classes. For global styles, use toastOptions on the Toaster component.

  5. Action buttons not working: Action buttons are defined in the action property of toast options. The onClick handler receives the toast ID for programmatic dismissal.

  6. TypeScript errors: Sonner has excellent TypeScript support. Make sure you're using the correct types for toast options and promise handlers.

Next Steps

Now that you have an advanced understanding of Sonner:

  • Explore advanced customization options and themes
  • Learn about different toast positions and animations
  • Implement toast queues and limits
  • Add custom toast components with JSX
  • Integrate with React Context for global toast management
  • Learn about TypeScript integration and type safety
  • Learn about other toast libraries (react-hot-toast, notistack)
  • Check the official documentation: https://sonner.emilkowal.ski/
  • Look for part 40 of this series for more advanced topics

Summary

You've successfully integrated Sonner into your React application with advanced features including promise handling, custom JSX content, action buttons, and comprehensive styling options. Sonner provides an opinionated, beautiful solution for displaying toast notifications with excellent developer experience.

SEO Keywords

sonner
React toast notifications
sonner tutorial
React notification library
sonner installation
React toast messages
sonner example
React alert notifications
sonner setup
React toast hooks
sonner customization
React notification system
sonner promise
React toast library
sonner getting started

Top comments (1)

Collapse
 
bhavin-allinonetools profile image
Bhavin Sheth

This is a really solid deep dive. I like that you didn’t stop at “how to show a toast” and instead treated notifications as a system, not a UI trick.

The custom hook (useToastNotifications) is especially nice — that’s the point where Sonner really starts to feel production-ready instead of just a demo library. Promise handling + consistent defaults is exactly how teams avoid toast chaos as apps grow.

Also appreciated the balance between:

opinionated defaults (Sonner’s strength)

and escape hatches (custom JSX, actions, infinite duration)

Quick question out of curiosity:
In larger apps, have you seen teams centralize toast usage even further (e.g. domain-specific helpers like notifyUserSaved, notifyPaymentFailed), or do most teams keep it generic like this and let features decide?

Great write-up — bookmarking this for the next time I refactor notification logic.