CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-sonner

An opinionated toast component for React providing comprehensive toast notifications with customization options

Pending
Overview
Eval results
Files

advanced-toast-features.mddocs/

Advanced Toast Features

Advanced toast functionality including promise integration, custom JSX content, action buttons, and state management utilities for complex notification scenarios.

Capabilities

Promise Toast Integration

Automatically manages toast states for asynchronous operations with loading, success, and error states.

/**
 * Creates a toast that tracks a promise's lifecycle
 * @param promise - Promise or function that returns a promise
 * @param options - Configuration for different promise states
 * @returns Object with unwrap method and toast ID
 */
function toast.promise<T>(
  promise: Promise<T> | (() => Promise<T>),
  options: PromiseData<T>
): { unwrap(): Promise<T> } & (string | number);

interface PromiseData<ToastData = any> {
  /** Message or component to show during loading state */
  loading?: string | React.ReactNode;
  /** Message, component, or function to show on success */
  success?: string | React.ReactNode | ((data: ToastData) => React.ReactNode | string | Promise<React.ReactNode | string>);
  /** Message, component, or function to show on error */
  error?: string | React.ReactNode | ((error: any) => React.ReactNode | string | Promise<React.ReactNode | string>);
  /** Description that can be dynamic based on result */
  description?: string | React.ReactNode | ((data: any) => React.ReactNode | string | Promise<React.ReactNode | string>);
  /** Callback executed regardless of promise outcome */
  finally?: () => void | Promise<void>;
}

Usage Examples:

import { toast } from "sonner";

// Basic promise toast
const uploadPromise = uploadFile(file);

toast.promise(uploadPromise, {
  loading: "Uploading file...",
  success: "File uploaded successfully!",
  error: "Failed to upload file"
});

// Dynamic success message
toast.promise(saveUserProfile(userData), {
  loading: "Saving profile...",
  success: (data) => `Profile saved! User ID: ${data.id}`,
  error: (error) => `Save failed: ${error.message}`
});

// With description and finally callback
toast.promise(processData(), {
  loading: "Processing your data...",
  success: "Data processed successfully",
  error: "Processing failed",
  description: (result) => `Processed ${result.count} items`,
  finally: () => {
    console.log("Processing completed");
    refreshUI();
  }
});

// Function-based promise
toast.promise(
  () => fetch("/api/data").then(res => res.json()),
  {
    loading: "Fetching data...",
    success: "Data loaded!",
    error: "Failed to load data"
  }
);

// Using unwrap to handle the original promise
const promiseToast = toast.promise(asyncOperation(), {
  loading: "Working...",
  success: "Done!",
  error: "Failed!"
});

// Access the original promise result
try {
  const result = await promiseToast.unwrap();
  console.log("Operation result:", result);
} catch (error) {
  console.error("Operation failed:", error);
}

Custom JSX Toasts

Create completely custom toast content using React components.

/**
 * Creates a toast with custom JSX content
 * @param jsx - Function that receives toast ID and returns React element
 * @param options - Additional configuration options
 * @returns Unique identifier for the created toast
 */
function toast.custom(
  jsx: (id: string | number) => React.ReactElement,
  options?: ExternalToast
): string | number;

Usage Examples:

import { toast } from "sonner";

// Simple custom toast
toast.custom((t) => (
  <div style={{ padding: "12px" }}>
    <h3>Custom Toast</h3>
    <p>This is completely custom content!</p>
    <button onClick={() => toast.dismiss(t)}>
      Close
    </button>
  </div>
));

// Complex custom toast with actions
toast.custom((t) => (
  <div className="custom-toast">
    <div className="toast-header">
      <strong>New Message</strong>
      <small>2 minutes ago</small>
    </div>
    <div className="toast-body">
      <p>You have received a new message from John Doe.</p>
    </div>
    <div className="toast-actions">
      <button 
        onClick={() => {
          viewMessage();
          toast.dismiss(t);
        }}
        className="btn-primary"
      >
        View
      </button>
      <button 
        onClick={() => toast.dismiss(t)}
        className="btn-secondary"
      >
        Dismiss
      </button>
    </div>
  </div>
));

// Custom toast with form
toast.custom((t) => {
  const [email, setEmail] = React.useState("");
  
  const handleSubmit = (e) => {
    e.preventDefault();
    subscribeToNewsletter(email);
    toast.dismiss(t);
    toast.success("Subscribed successfully!");
  };
  
  return (
    <form onSubmit={handleSubmit} className="newsletter-toast">
      <h4>Subscribe to Newsletter</h4>
      <input
        type="email"
        placeholder="Enter your email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <div className="form-actions">
        <button type="submit">Subscribe</button>
        <button type="button" onClick={() => toast.dismiss(t)}>
          Cancel
        </button>
      </div>
    </form>
  );
});

// Custom toast with options
toast.custom(
  (t) => <CustomNotificationComponent id={t} />,
  {
    duration: Infinity, // Keep open until manually dismissed
    position: "top-center"
  }
);

Toast State Management

Functions for managing toast state and retrieving toast information.

/**
 * Gets all toasts in history (including dismissed ones)
 * @returns Array of all toast objects
 */
function toast.getHistory(): ToastT[];

/**
 * Gets currently active (visible) toasts
 * @returns Array of active toast objects
 */
function toast.getToasts(): ToastT[];

Usage Examples:

import { toast } from "sonner";

// Check current toast count
const activeToasts = toast.getToasts();
console.log(`Currently showing ${activeToasts.length} toasts`);

// Conditional toast creation
if (toast.getToasts().length < 3) {
  toast("New notification");
} else {
  console.log("Too many toasts, skipping...");
}

// Get toast history for analytics
const allToasts = toast.getHistory();
const errorToasts = allToasts.filter(t => t.type === "error");
console.log(`User saw ${errorToasts.length} error messages`);

// Find specific toast
const loadingToasts = toast.getToasts().filter(t => t.type === "loading");
if (loadingToasts.length > 0) {
  console.log("Operations still in progress");
}

Toast Updates and Management

Update existing toasts or manage multiple related toasts.

// Update existing toast by providing same ID
const toastId = toast("Initial message", { duration: 10000 });

// Later, update the same toast
toast("Updated message", { id: toastId });

// Update with different type
toast.success("Completed!", { id: toastId });

Usage Examples:

import { toast } from "sonner";

// Progress tracking
const progressId = toast.loading("Starting process...");

setTimeout(() => {
  toast.loading("Step 1 of 3: Downloading...", { id: progressId });
}, 1000);

setTimeout(() => {
  toast.loading("Step 2 of 3: Processing...", { id: progressId });
}, 3000);

setTimeout(() => {
  toast.success("Process completed!", { id: progressId });
}, 5000);

// Multi-step form handling
let formToastId: string | number;

const handleFormStart = () => {
  formToastId = toast.loading("Validating form...");
};

const handleFormValidated = () => {
  toast.loading("Submitting data...", { id: formToastId });
};

const handleFormComplete = (result) => {
  if (result.success) {
    toast.success("Form submitted successfully!", { id: formToastId });
  } else {
    toast.error(`Submission failed: ${result.error}`, { id: formToastId });
  }
};

// Batch operations
const batchId = toast.loading("Processing 0/100 items...");

for (let i = 0; i < 100; i++) {
  await processItem(i);
  
  // Update progress every 10 items
  if (i % 10 === 0) {
    toast.loading(`Processing ${i}/100 items...`, { id: batchId });
  }
}

toast.success("All items processed!", { id: batchId });

Advanced Patterns

Promise Error Handling

// Handle different HTTP error codes
toast.promise(apiCall(), {
  loading: "Saving...",
  success: "Saved successfully!",
  error: (error) => {
    if (error.status === 401) {
      return "Please log in to continue";
    } else if (error.status === 403) {
      return "You don't have permission to do this";
    } else if (error.status >= 500) {
      return "Server error. Please try again later";
    }
    return "Something went wrong";
  }
});

// Promise with retry functionality
const attemptOperation = async (retryCount = 0) => {
  try {
    const result = await riskyOperation();
    return result;
  } catch (error) {
    if (retryCount < 3) {
      throw error; // Let promise toast handle it
    }
    throw new Error("Max retries exceeded");
  }
};

toast.promise(attemptOperation(), {
  loading: "Attempting operation...",
  success: "Operation successful!",
  error: (error) => {
    return (
      <div>
        <p>Operation failed: {error.message}</p>
        <button onClick={() => toast.promise(attemptOperation(), { /* ... */ })}>
          Retry
        </button>
      </div>
    );
  }
});

Complex Custom Components

// Toast with image and rich content
const MediaToast = ({ id, media, onAction }) => (
  <div className="media-toast">
    <img src={media.thumbnail} alt="" className="media-thumbnail" />
    <div className="media-content">
      <h4>{media.title}</h4>
      <p>{media.description}</p>
      <div className="media-actions">
        <button onClick={() => onAction('play')}>Play</button>
        <button onClick={() => onAction('download')}>Download</button>
        <button onClick={() => toast.dismiss(id)}>×</button>
      </div>
    </div>
  </div>
);

// Usage
toast.custom((id) => (
  <MediaToast 
    id={id}
    media={mediaItem}
    onAction={(action) => handleMediaAction(action, mediaItem)}
  />
));

// Toast with live updates
const LiveUpdateToast = ({ id, initialData }) => {
  const [data, setData] = React.useState(initialData);
  
  React.useEffect(() => {
    const subscription = subscribeToUpdates((newData) => {
      setData(newData);
    });
    
    return () => subscription.unsubscribe();
  }, []);
  
  return (
    <div className="live-toast">
      <h4>Live Data: {data.value}</h4>
      <small>Last updated: {data.timestamp}</small>
      <button onClick={() => toast.dismiss(id)}>Close</button>
    </div>
  );
};

Toast Orchestration

// Sequential toasts
const showSequentialToasts = async () => {
  const step1 = toast.loading("Step 1: Preparing...");
  await delay(2000);
  
  toast.success("Step 1 complete", { id: step1 });
  await delay(1000);
  
  const step2 = toast.loading("Step 2: Processing...");
  await delay(3000);
  
  toast.success("Step 2 complete", { id: step2 });
  await delay(1000);
  
  toast.success("All steps completed!");
};

// Conditional toast chains
const handleUserAction = async (action) => {
  const loadingId = toast.loading(`Processing ${action}...`);
  
  try {
    const result = await performAction(action);
    
    if (result.requiresConfirmation) {
      toast.custom((id) => (
        <ConfirmationToast
          message={result.confirmationMessage}
          onConfirm={() => {
            toast.dismiss(id);
            finalizeAction(result);
          }}
          onCancel={() => toast.dismiss(id)}
        />
      ), { id: loadingId });
    } else {
      toast.success("Action completed!", { id: loadingId });
    }
  } catch (error) {
    toast.error(`Failed to ${action}`, { id: loadingId });
  }
};

Types

interface ToastT {
  id: number | string;
  title?: (() => React.ReactNode) | React.ReactNode;
  type?: "normal" | "action" | "success" | "info" | "warning" | "error" | "loading" | "default";
  icon?: React.ReactNode;
  jsx?: React.ReactNode;
  richColors?: boolean;
  invert?: boolean;
  closeButton?: boolean;
  dismissible?: boolean;
  description?: (() => React.ReactNode) | React.ReactNode;
  duration?: number;
  delete?: boolean;
  action?: Action | React.ReactNode;
  cancel?: Action | React.ReactNode;
  onDismiss?: (toast: ToastT) => void;
  onAutoClose?: (toast: ToastT) => void;
  promise?: Promise<any> | (() => Promise<any>);
  cancelButtonStyle?: React.CSSProperties;
  actionButtonStyle?: React.CSSProperties;
  style?: React.CSSProperties;
  unstyled?: boolean;
  className?: string;
  classNames?: ToastClassnames;
  descriptionClassName?: string;
  position?: Position;
}

Install with Tessl CLI

npx tessl i tessl/npm-sonner

docs

advanced-toast-features.md

core-toast-functions.md

index.md

react-hooks.md

toaster-component.md

tile.json