Skip to main content

Feedback Components

Toast notifications and alert systems for user feedback.

Toast

Toast notifications for temporary messages.

Preview

Live Editor
function ToastDemo() {
  const [toasts, setToasts] = React.useState([]);
  
  const addToast = (type) => {
    const id = Date.now();
    setToasts(prev => [...prev, { id, type }]);
    setTimeout(() => {
      setToasts(prev => prev.filter(t => t.id !== id));
    }, 3000);
  };
  
  return (
    <div>
      <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
        <button
          onClick={() => addToast('default')}
          style={{
            padding: '10px 20px',
            borderRadius: '8px',
            background: '#1e293b',
            color: 'white',
            border: 'none',
            cursor: 'pointer',
            fontWeight: 600,
          }}
        >
          Show Toast
        </button>
        <button
          onClick={() => addToast('success')}
          style={{
            padding: '10px 20px',
            borderRadius: '8px',
            background: '#22c55e',
            color: 'white',
            border: 'none',
            cursor: 'pointer',
            fontWeight: 600,
          }}
        >
          Success Toast
        </button>
        <button
          onClick={() => addToast('error')}
          style={{
            padding: '10px 20px',
            borderRadius: '8px',
            background: '#ef4444',
            color: 'white',
            border: 'none',
            cursor: 'pointer',
            fontWeight: 600,
          }}
        >
          Error Toast
        </button>
      </div>
      
      <div style={{
        position: 'fixed',
        bottom: '24px',
        right: '24px',
        display: 'flex',
        flexDirection: 'column',
        gap: '8px',
        zIndex: 9999,
      }}>
        {toasts.map((toast) => (
          <div
            key={toast.id}
            style={{
              padding: '16px 20px',
              borderRadius: '8px',
              background: 'white',
              boxShadow: '0 10px 40px -10px rgba(0,0,0,0.2)',
              border: '1px solid #e2e8f0',
              display: 'flex',
              alignItems: 'center',
              gap: '12px',
              minWidth: '300px',
              animation: 'slideIn 0.3s ease-out',
            }}
          >
            <span style={{
              width: '8px',
              height: '8px',
              borderRadius: '50%',
              background: toast.type === 'success' ? '#22c55e' : toast.type === 'error' ? '#ef4444' : '#8b5cf6',
            }} />
            <div>
              <div style={{ fontWeight: 600, fontSize: '14px' }}>
                {toast.type === 'success' ? 'Success!' : toast.type === 'error' ? 'Error' : 'Notification'}
              </div>
              <div style={{ fontSize: '13px', color: '#64748b' }}>
                {toast.type === 'success' ? 'Your changes have been saved.' : 
                 toast.type === 'error' ? 'Something went wrong.' : 
                 'You have a new message.'}
              </div>
            </div>
          </div>
        ))}
      </div>
      
      <style>{`
        @keyframes slideIn {
          from { transform: translateX(100%); opacity: 0; }
          to { transform: translateX(0); opacity: 1; }
        }
      `}</style>
    </div>
  );
}
Result
Loading...

Usage

import { 
Toast,
ToastProvider,
ToastViewport,
ToastTitle,
ToastDescription,
ToastAction,
ToastClose,
useToast,
toast,
Toaster,
} from "@cpod/react";

// 1. Add Toaster to your app root
function App() {
return (
<>
<YourApp />
<Toaster />
</>
);
}

// 2. Use the toast function anywhere
function MyComponent() {
const { toast } = useToast();

return (
<Button
onClick={() => {
toast({
title: "Scheduled: Catch up",
description: "Friday, February 10, 2023 at 5:57 PM",
});
}}
>
Show Toast
</Button>
);
}

// With action button
toast({
title: "Undo",
description: "Message deleted",
action: (
<ToastAction altText="Undo">Undo</ToastAction>
),
});

// Destructive variant
toast({
variant: "destructive",
title: "Error",
description: "Something went wrong.",
});

API

useToast Hook:

const { toast, dismiss, toasts } = useToast();

// Show toast
toast({
title: "Title",
description: "Description",
variant: "default" | "destructive",
action: <ToastAction altText="Action">Action</ToastAction>,
});

// Dismiss specific toast
dismiss(toastId);

// Dismiss all
dismiss();

Toaster Component

The Toaster component renders the toast viewport and should be added to your app root:

import { Toaster } from "@cpod/react";

function App() {
return (
<>
<YourApp />
<Toaster />
</>
);
}

Toaster Props:

PropTypeDefaultDescription
positiontop-left | top-right | bottom-left | bottom-right | top-center | bottom-centerbottom-rightToast position
durationnumber5000Auto-dismiss duration (ms)

Sonner

Enhanced toast notifications with the Sonner library.

Preview

Live Editor
function SonnerDemo() {
  const [toasts, setToasts] = React.useState([]);
  
  const showToast = (type) => {
    const id = Date.now();
    const messages = {
      default: { title: 'Event created', desc: 'Sunday, December 03, 2023 at 9:00 AM' },
      success: { title: 'Success', desc: 'Your changes have been saved successfully.' },
      error: { title: 'Error', desc: 'There was a problem with your request.' },
      loading: { title: 'Loading...', desc: 'Please wait while we process your request.' },
    };
    
    setToasts(prev => [...prev, { id, type, ...messages[type] }]);
    
    if (type !== 'loading') {
      setTimeout(() => {
        setToasts(prev => prev.filter(t => t.id !== id));
      }, 4000);
    }
  };
  
  return (
    <div>
      <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
        {['default', 'success', 'error', 'loading'].map((type) => (
          <button
            key={type}
            onClick={() => showToast(type)}
            style={{
              padding: '10px 20px',
              borderRadius: '8px',
              background: type === 'success' ? '#22c55e' : 
                         type === 'error' ? '#ef4444' : 
                         type === 'loading' ? '#f59e0b' : '#8b5cf6',
              color: 'white',
              border: 'none',
              cursor: 'pointer',
              fontWeight: 600,
              textTransform: 'capitalize',
            }}
          >
            {type}
          </button>
        ))}
      </div>
      
      <div style={{
        position: 'fixed',
        bottom: '24px',
        right: '24px',
        display: 'flex',
        flexDirection: 'column',
        gap: '8px',
        zIndex: 9999,
      }}>
        {toasts.map((toast) => (
          <div
            key={toast.id}
            style={{
              padding: '16px',
              borderRadius: '12px',
              background: 'white',
              boxShadow: '0 10px 40px -10px rgba(0,0,0,0.2)',
              border: '1px solid #e2e8f0',
              display: 'flex',
              alignItems: 'flex-start',
              gap: '12px',
              minWidth: '320px',
              animation: 'slideIn 0.3s ease-out',
            }}
          >
            <div style={{
              width: '24px',
              height: '24px',
              borderRadius: '50%',
              background: toast.type === 'success' ? '#dcfce7' : 
                         toast.type === 'error' ? '#fee2e2' : 
                         toast.type === 'loading' ? '#fef3c7' : '#f3e8ff',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              fontSize: '12px',
              flexShrink: 0,
            }}>
              {toast.type === 'success' ? '✓' : 
               toast.type === 'error' ? '✕' : 
               toast.type === 'loading' ? '◌' : '🔔'}
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '2px' }}>
                {toast.title}
              </div>
              <div style={{ fontSize: '13px', color: '#64748b' }}>
                {toast.desc}
              </div>
            </div>
            <button
              onClick={() => setToasts(prev => prev.filter(t => t.id !== toast.id))}
              style={{
                background: 'none',
                border: 'none',
                cursor: 'pointer',
                color: '#94a3b8',
                fontSize: '16px',
                padding: 0,
              }}
            >
              ×
            </button>
          </div>
        ))}
      </div>
      
      <style>{`
        @keyframes slideIn {
          from { transform: translateX(100%); opacity: 0; }
          to { transform: translateX(0); opacity: 1; }
        }
      `}</style>
    </div>
  );
}
Result
Loading...

Usage

import { Sonner } from "@cpod/react";
import { toast } from "sonner";

// Add Sonner to your app root
function App() {
return (
<ThemeProvider>
<YourApp />
<Sonner />
</ThemeProvider>
);
}

// Use toast() from sonner package
function MyComponent() {
return (
<Button onClick={() => toast("Event has been created")}>
Show Toast
</Button>
);
}

// Different types
toast.success("Your changes have been saved");
toast.error("Something went wrong");
toast.loading("Loading...");
toast.info("Did you know?");

// With description
toast("Event created", {
description: "Sunday, December 03, 2023 at 9:00 AM",
});

// With action
toast("Deleted", {
action: {
label: "Undo",
onClick: () => console.log("Undo"),
},
});

// Promise toast
toast.promise(saveData(), {
loading: "Saving...",
success: "Saved!",
error: "Error saving data",
});

Props

Sonner Component:

PropTypeDefaultDescription
positionstringbottom-rightToast position
expandbooleanfalseExpand toasts by default
richColorsbooleanfalseUse rich colors for types
durationnumber4000Default duration in ms
closeButtonbooleanfalseShow close button

Why Sonner?

Sonner provides additional features over the basic Toast:

  • Promise handling - Automatic loading/success/error states
  • Rich animations - Smooth enter/exit animations
  • Theme integration - Works with ThemeProvider
  • Stacking - Multiple toasts stack elegantly
  • Positioning - Multiple position options
  • Actions - Built-in action buttons

Best Practices

1. Choose the Right Component

Use CaseComponent
Simple notificationsToast
Rich notifications with actionsSonner
Async operation feedbackSonner with promises
Error messagesBoth with destructive variant

2. Toast Duration Guidelines

TypeDurationReason
Success3-4 secondsQuick confirmation
Info4-5 secondsTime to read
Error5-7 secondsImportant, needs attention
LoadingUntil completeUser needs to wait

3. Action Buttons

// Good - Clear action
toast({
title: "Message deleted",
action: <ToastAction altText="Undo delete">Undo</ToastAction>,
});

// Avoid - Too many actions
toast({
title: "New message",
action: (
<>
<button>Reply</button>
<button>Archive</button>
<button>Delete</button>
</>
),
});

4. Error Handling

try {
await saveData();
toast.success("Saved successfully");
} catch (error) {
toast.error("Failed to save", {
description: error.message,
action: {
label: "Retry",
onClick: () => saveData(),
},
});
}