Stacked toasts on Sonner styled with theme tokens: status icons, actions, promises, swipe to dismiss, pause on hover and focus, and screen-reader announcements.
Notifications
Each button shows a different toast.
"use client";
import { buttonVariants } from "@/components/ballmac/button";
import { Toaster, toast } from "@/components/ballmac/toast";
const btn = buttonVariants({ variant: "outline", size: "sm" });
export default function ToastDemo() {
return (
<div className="grid w-full max-w-sm gap-3 rounded-xl border bg-card p-5 shadow-sm">
<div>
<p className="text-sm font-semibold">Notifications</p>
<p className="text-xs text-muted-foreground">Each button shows a different toast.</p>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"
className={btn}
onClick={() => toast("Event created", { description: "Launch review, Friday at 10:00." })}
>
Default
</button>
<button type="button" className={btn} onClick={() => toast.success("Changes saved")}>
Success
</button>
<button
type="button"
className={btn}
onClick={() => toast.error("Upload failed", { description: "The file is larger than 10 MB." })}
>
Error
</button>
<button
type="button"
className={btn}
onClick={() =>
toast("Conversation archived", { action: { label: "Undo", onClick: () => toast.success("Restored") } })
}
>
Undo
</button>
<button
type="button"
className={btn}
onClick={() =>
toast.promise(new Promise((r) => setTimeout(r, 1600)), {
loading: "Publishing site…",
success: "Site published",
error: "Could not publish",
})
}
>
Promise
</button>
</div>
<Toaster position="bottom-right" />
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/toastInstall the dependencies.
$ pnpm add sonner@^2 lucide-reactCopy the source into your project.
components/ballmac/toast.tsx// Ballmac UI: Toast. https://ui.ballmac.com/components/toast // Based on shadcn/ui Sonner (MIT, Copyright (c) 2023 shadcn) on Sonner (MIT, Copyright (c) 2023 Emil Kowalski), restyled with theme tokens (so it follows the site's light and dark mode without a theme provider), status icons, and accessible action buttons. "use client"; import * as React from "react"; import { CircleAlert, CircleCheck, Info, Loader, TriangleAlert, X } from "lucide-react"; import { Toaster as Sonner, toast } from "sonner"; type ToasterProps = React.ComponentProps<typeof Sonner>; /** * Mount once near the root of your app. Call `toast("Saved")`, `toast.success(...)`, `toast.error(...)`, * `toast.promise(...)` or `toast.custom(...)` from anywhere. Toasts are announced to screen readers, * pause while hovered or focused, and can be dismissed with Escape after pressing Alt+T to focus the region. */ function Toaster({ toastOptions, icons, ...props }: ToasterProps) { return ( <Sonner className="toaster group" closeButton icons={{ success: <CircleCheck aria-hidden="true" className="size-4 text-chart-2" />, info: <Info aria-hidden="true" className="size-4 text-primary" />, warning: <TriangleAlert aria-hidden="true" className="size-4 text-chart-3" />, error: <CircleAlert aria-hidden="true" className="size-4 text-destructive" />, loading: <Loader aria-hidden="true" className="size-4 animate-spin motion-reduce:animate-none" />, close: <X aria-hidden="true" className="size-3.5" />, ...icons, }} style={ { "--normal-bg": "var(--popover)", "--normal-text": "var(--popover-foreground)", "--normal-border": "var(--border)", "--border-radius": "var(--radius-xl, 0.75rem)", } as React.CSSProperties } toastOptions={{ ...toastOptions, classNames: { toast: "!shadow-[0_12px_36px_-10px_rgb(0_0_0/0.25)] !gap-3 !p-4 !font-sans", title: "!text-sm !font-medium", description: "!text-sm !text-muted-foreground", actionButton: "!bg-primary !text-primary-foreground !rounded-md !h-8 !px-3 !text-[13px] !font-medium focus-visible:!ring-[3px] focus-visible:!ring-ring/50", cancelButton: "!bg-muted !text-foreground !rounded-md !h-8 !px-3 !text-[13px] !font-medium focus-visible:!ring-[3px] focus-visible:!ring-ring/50", closeButton: "!bg-popover !border-border !text-muted-foreground hover:!text-foreground", ...toastOptions?.classNames, }, }} {...props} /> ); } export { Toaster, toast, type ToasterProps };Update the import paths to match your project setup.
Usage
import { Toaster, toast } from "@/components/ballmac/toast"The full example is in the Code tab above.
Examples
Action and warning
"use client";
import { buttonVariants } from "@/components/ballmac/button";
import { Toaster, toast } from "@/components/ballmac/toast";
const id = "toast-states";
export default function ToastStates() {
return (
<div className="flex flex-wrap gap-2">
<button
type="button"
className={buttonVariants({ variant: "outline", size: "sm" })}
onClick={() =>
toast.info("New version available", {
toasterId: id,
description: "Reload to get the latest changes.",
duration: 8000,
action: { label: "Reload", onClick: () => undefined },
cancel: { label: "Later", onClick: () => undefined },
})
}
>
Action and cancel
</button>
<button
type="button"
className={buttonVariants({ variant: "outline", size: "sm" })}
onClick={() => toast.warning("Storage almost full", { toasterId: id, description: "You have used 92% of your plan." })}
>
Warning
</button>
<Toaster id={id} position="top-center" />
</div>
);
}Accessibility
| Key | Action |
|---|---|
| Alt+T | Focuses the notification region |
| Tab | Reaches toast actions and close buttons |
| Screen readers | Toasts are announced politely |
Use with AI
Render <Toaster /> once, then call toast(), toast.success(), toast.error() or toast.promise() from anywhere. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Toast (@ballmac/toast) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Confirming an action (saved, copied, archived)
- Undo prompts
- Background task results
Not for
- Errors the user must fix in a form; use field errors
- Permanent page messages; use alert or banner
- A macOS-style stack; use toast-stack
Registry JSON: https://ui.ballmac.com/r/toast.json
Credits
Based on shadcn/ui Sonner, adapted by Ballmac. Free to use in personal and commercial projects.
- Registry
- None
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Toast Stack
A token-based notification stack with dismissal, optional actions, and pause-on-interaction timing.
Alert Dialog
Focus-managed confirmation for consequential actions, with a clear cancel path, optional media, and a token-based destructive action.
Alert
A semantic callout with five theme-aware tones, clear icon placement, an action row, and opt-in urgent announcements.