LLM index: /llms.txt

Toaster

Headless toast notifications with variant-aware styling and custom action/cancel controls; Powered by Sonner.

"use client";

import { toast, Toaster } from "@/components/nexus-ui/toaster";
import { Button } from "@/components/ui/button";

const ToasterDefault = () => {
  const toasterId = "toaster-example-default";

  return (
    <div className="flex min-h-24 w-full items-center justify-center">
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("Message copied to clipboard.", {
            toasterId,
          })
        }
      >
        Show toast
      </Button>
      <Toaster id={toasterId} />
    </div>
  );
};

export default ToasterDefault;

Installation

Run the following command:
npx shadcn@latest add @nexus-ui/toaster
Add the Toaster component
app/layout.tsx
import { Toaster } from "@/components/nexus-ui/toaster"

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head />
      <body>
        <main>{children}</main>
        <Toaster />
      </body>
    </html>
  )
}
Install the following dependencies:
npx shadcn@latest add button && npm install @hugeicons/core-free-icons @hugeicons/react next-themes sonner
Copy and paste the following code into your project.
components/nexus-ui/toaster.tsx
"use client";

import {
  Alert02Icon,
  Cancel01Icon,
  CheckmarkCircle01Icon,
  InformationCircleIcon,
  Loading03Icon,
  OctagonXIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import type * as React from "react";
import { useTheme } from "next-themes";
import {
  Toaster as Sonner,
  toast as sonnerToast,
  type ExternalToast,
  type ToasterProps,
} from "sonner";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

type ToastVariant =
  | "default"
  | "success"
  | "info"
  | "warning"
  | "error"
  | "loading";

type ToastAction = {
  label: React.ReactNode;
  onClick?: () => void;
};

type ToastContent = {
  title: React.ReactNode;
  description?: React.ReactNode;
  variant?: ToastVariant;
  icon?: React.ReactNode | null;
  action?: ToastAction;
  cancel?: ToastAction;
} & Omit<
  ExternalToast,
  "icon" | "classNames" | "unstyled" | "action" | "cancel"
>;

const variantIconMap: Partial<Record<ToastVariant, React.ReactNode>> = {
  success: (
    <HugeiconsIcon
      icon={CheckmarkCircle01Icon}
      strokeWidth={2}
      className="size-4.5"
    />
  ),
  info: (
    <HugeiconsIcon
      icon={InformationCircleIcon}
      strokeWidth={2}
      className="size-4.5"
    />
  ),
  warning: (
    <HugeiconsIcon icon={Alert02Icon} strokeWidth={2} className="size-4.5" />
  ),
  error: (
    <HugeiconsIcon icon={OctagonXIcon} strokeWidth={2} className="size-4.5" />
  ),
  loading: (
    <HugeiconsIcon
      icon={Loading03Icon}
      strokeWidth={2}
      className="size-4.5 animate-spin"
    />
  ),
};

const toast = {
  custom: (content: ToastContent) => {
    const {
      title,
      description,
      variant,
      icon,
      action,
      cancel,
      dismissible,
      closeButton,
      ...sonnerOptions
    } = content;
    return sonnerToast.custom(
      (id) => (
        <ToastCard
          id={id}
          title={title}
          description={description}
          variant={variant}
          icon={icon}
          action={action}
          cancel={cancel}
          dismissible={dismissible}
          closeButton={closeButton}
        />
      ),
      {
        ...sonnerOptions,
        dismissible,
        closeButton,
      },
    );
  },
  default: (
    title: React.ReactNode,
    options?: Omit<ToastContent, "title" | "variant">,
  ) => toast.custom({ title, variant: "default", ...options }),
  success: (
    title: React.ReactNode,
    options?: Omit<ToastContent, "title" | "variant">,
  ) => toast.custom({ title, variant: "success", ...options }),
  info: (
    title: React.ReactNode,
    options?: Omit<ToastContent, "title" | "variant">,
  ) => toast.custom({ title, variant: "info", ...options }),
  warning: (
    title: React.ReactNode,
    options?: Omit<ToastContent, "title" | "variant">,
  ) => toast.custom({ title, variant: "warning", ...options }),
  error: (
    title: React.ReactNode,
    options?: Omit<ToastContent, "title" | "variant">,
  ) => toast.custom({ title, variant: "error", ...options }),
  loading: (
    title: React.ReactNode,
    options?: Omit<ToastContent, "title" | "variant">,
  ) => toast.custom({ title, variant: "loading", ...options }),
  dismiss: sonnerToast.dismiss,
};

function ToastCard({
  id,
  title,
  description,
  icon,
  action,
  cancel,
  dismissible = true,
  closeButton = true,
  variant = "default",
}: ToastContent & { id: string | number }) {
  const resolvedIcon = icon === null ? null : (icon ?? variantIconMap[variant]);
  const canDismiss = dismissible !== false;

  return (
    <div
      className={cn(
        "relative flex w-full items-start justify-between gap-2 rounded-lg px-4 py-3 shadow-[0_8px_10px_rgb(0,0,0,0.02)] transition-colors lg:w-90 xl:w-120",
        "border border-(--toast-color)/5 bg-(--toast-bg) text-(--toast-color)",
        "dark:bg-(--toast-bg)",
        "[--toast-bg:var(--popover)] [--toast-color:var(--popover-foreground)]",
        "data-[variant=default]:[--toast-bg:var(--popover)] data-[variant=default]:[--toast-color:var(--primary)]",
        "data-[variant=success]:[--toast-bg:#F0FDF4] data-[variant=success]:[--toast-color:#16A34A] data-[variant=success]:dark:[--toast-bg:#17221C] data-[variant=success]:dark:[--toast-color:#15803D]",
        "data-[variant=info]:[--toast-bg:#EFF6FF] data-[variant=info]:[--toast-color:#2563EB] data-[variant=info]:dark:[--toast-bg:#181D28] data-[variant=info]:dark:[--toast-color:#1D4ED8]",
        "data-[variant=warning]:[--toast-bg:#FEFCE8] data-[variant=warning]:[--toast-color:#CA8A04] data-[variant=warning]:dark:[--toast-bg:#252015] data-[variant=warning]:dark:[--toast-color:#CA8A04]",
        "data-[variant=error]:[--toast-bg:#FEF2F2] data-[variant=error]:[--toast-color:#DC2626] data-[variant=error]:dark:[--toast-bg:#271818] data-[variant=error]:dark:[--toast-color:#B91C1C]",
      )}
      data-variant={variant}
    >
      {resolvedIcon ? (
        <div className="flex size-6 shrink-0 items-center justify-center text-(--toast-color)">
          {resolvedIcon}
        </div>
      ) : null}

      <div className="flex w-full flex-col gap-0.25">
        <span className="text-sm leading-6 font-medium text-(--toast-color)">
          {title}
        </span>
        {description ? (
          <div
            className="mt-0 text-sm leading-5.5 font-[350] text-(--toast-color) data-[variant=default]:text-muted-foreground"
            data-variant={variant}
          >
            {description}
          </div>
        ) : null}

        <div className="flex items-center gap-1.5">
          {action ? (
            <Button
              type="button"
              variant="default"
              size="sm"
              className={cn(
                "mt-2 inline-flex w-fit cursor-pointer items-center justify-center rounded-full text-[13px] font-[450] transition-colors",
                "bg-(--toast-color) text-(--toast-bg) hover:bg-(--toast-color)/90 hover:text-(--toast-bg)",
              )}
              onClick={() => {
                action.onClick?.();
                if (canDismiss) sonnerToast.dismiss(id);
              }}
            >
              {action.label}
            </Button>
          ) : null}

          {cancel ? (
            <Button
              type="button"
              variant="outline"
              size="sm"
              className={cn(
                "mt-2 inline-flex w-fit cursor-pointer items-center justify-center rounded-full text-[13px] font-[450] transition-colors",
                "border border-(--toast-color)/10 text-(--toast-color) hover:bg-(--toast-color)/5 dark:hover:bg-(--toast-color)/10",
              )}
              onClick={() => {
                cancel.onClick?.();
                if (canDismiss) sonnerToast.dismiss(id);
              }}
            >
              {cancel.label}
            </Button>
          ) : null}
        </div>
      </div>

      {canDismiss && closeButton !== false ? (
        <Button
          type="button"
          variant="ghost"
          size="icon-sm"
          aria-label="Close notification"
          className={cn(
            "inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-full text-(--toast-color)/70 transition-colors",
            "hover:bg-(--toast-color)/10 hover:text-(--toast-color) focus-visible:ring-2 focus-visible:ring-(--toast-color)/35 dark:hover:bg-(--toast-color)/10 dark:hover:text-(--toast-color)",
          )}
          onClick={() => sonnerToast.dismiss(id)}
        >
          <HugeiconsIcon
            icon={Cancel01Icon}
            strokeWidth={2}
            className="size-4"
          />
        </Button>
      ) : null}
    </div>
  );
}

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = "system" } = useTheme();

  return (
    <Sonner
      theme={theme as ToasterProps["theme"]}
      className="toaster group"
      toastOptions={{ unstyled: true }}
      {...props}
    />
  );
};

export { toast, Toaster };
Add the Toaster component
app/layout.tsx
import { Toaster } from "@/components/nexus-ui/toaster"

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head />
      <body>
        <Toaster />
        <main>{children}</main>
      </body>
    </html>
  )
}

Usage

import { toast } from "@/components/nexus-ui/toaster";
toast.default("Event has been created.");

Examples

With Description

Add supporting copy with the description option.

"use client";

import { toast, Toaster } from "@/components/nexus-ui/toaster";
import { Button } from "@/components/ui/button";

const ToasterWithDescription = () => {
  const toasterId = "toaster-example-with-description";

  return (
    <div className="flex min-h-24 w-full items-center justify-center">
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("Conversation exported.", {
            description: "Downloaded `session-2026-05-13.json` with messages and citations.",
            toasterId,
          })
        }
      >
        Show toast
      </Button>
      <Toaster id={toasterId} />
    </div>
  );
};

export default ToasterWithDescription;

With Action

Add an inline CTA using the action option.

"use client";

import { toast, Toaster } from "@/components/nexus-ui/toaster";
import { Button } from "@/components/ui/button";

const ToasterWithAction = () => {
  const toasterId = "toaster-example-with-action";

  return (
    <div className="flex min-h-24 w-full items-center justify-center">
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("Prompt preset updated.", {
            description:
              "Saved `research-mode` as your default assistant preset.",
            toasterId,
            action: {
              label: "Revert",
              onClick: () => console.log("Preset reverted"),
            },
          })
        }
      >
        Show toast
      </Button>
      <Toaster id={toasterId} />
    </div>
  );
};

export default ToasterWithAction;

Variants

Change the variant on each toast call to control the feedback type and visual style.

"use client";

import { toast, Toaster } from "@/components/nexus-ui/toaster";
import { Button } from "@/components/ui/button";

const ToasterVariantsRow = () => {
  const toasterId = "toaster-example-variants";

  return (
    <div className="flex min-h-24 w-full flex-wrap items-center justify-center gap-2">
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.success("Deploy completed.", {
            description: "Your chat assistant changes are now live in production.",
            toasterId,
          })
        }
      >
        Success
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.info("Model switched.", {
            description: "Responses will now use `gpt-5.5-medium` for this thread.",
            toasterId,
          })
        }
      >
        Info
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.warning("Context window almost full.", {
            description: "Older messages may be truncated in the next response.",
            toasterId,
          })
        }
      >
        Warning
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.error("Tool call failed.", {
            description: "The web search tool timed out. Try again in a moment.",
            toasterId,
          })
        }
      >
        Error
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.loading("Generating answer...", {
            description: "The assistant is analyzing files and drafting a response.",
            toasterId,
          })
        }
      >
        Loading
      </Button>
      <Toaster id={toasterId} />
    </div>
  );
};

export default ToasterVariantsRow;

Position

Show toasts in different corners and center positions using per-toast position.

"use client";

import { toast, Toaster } from "@/components/nexus-ui/toaster";
import { Button } from "@/components/ui/button";

const ToasterPosition = () => {
  const toasterId = "toaster-example-position";

  return (
    <div className="flex min-h-24 w-full flex-wrap items-center justify-center gap-2">
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("Sync started.", {
            description: "Top-left toast for global workspace sync updates.",
            position: "top-left",
            toasterId,
          })
        }
      >
        Top Left
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("Agent requested approval.", {
            description:
              "Top-center toast for high-priority permission prompts.",
            position: "top-center",
            toasterId,
          })
        }
      >
        Top Center
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("Update available.", {
            description:
              "Top-right toast for product announcements and releases.",
            position: "top-right",
            toasterId,
          })
        }
      >
        Top Right
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("File indexed.", {
            description: "Bottom-left toast for background indexing events.",
            position: "bottom-left",
            toasterId,
          })
        }
      >
        Bottom Left
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("Draft saved.", {
            description:
              "Bottom-center toast for editor autosave confirmations.",
            position: "bottom-center",
            toasterId,
          })
        }
      >
        Bottom Center
      </Button>
      <Button
        variant="secondary"
        size="default"
        type="button"
        onClick={() =>
          toast.default("Prompt copied.", {
            description: "Bottom-right toast for quick local action feedback.",
            position: "bottom-right",
            toasterId,
          })
        }
      >
        Bottom Right
      </Button>
      <Toaster id={toasterId} />
    </div>
  );
};

export default ToasterPosition;

API Reference

toast

Custom headless toast helper built on sonnerToast.custom. It renders the Toast UI while forwarding supported Sonner toast options for behavior and lifecycle.

Prop

Type

Toaster

Custom wrapper that mounts Sonner and syncs theme from next-themes, with toastOptions={{ unstyled: true }} by default.

Prop

Type

For advanced usage and full configuration options, see the Sonner docs.

View as markdown Edit on GitHub