Sonner
An opinionated toast notification system for React. Built on sonner with UI.MD theming.
Installation
$
Usage
import { Toaster } from "@/components/ui/sonner"
import { toast } from "sonner"Examples
Default
Mount one <Toaster /> at the root of your app, then call toast() from anywhere to fire a notification.
Positioning
Set where toasts land with position on <Toaster /> (top/bottom paired with left/center/right). Pick a cell and the code below updates to match your selection.
Toast position
position="bottom-right"
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| position | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "bottom-right" | Where toasts are anchored on screen. |
| richColors | boolean | false | Enables sonner's built-in colored success/error styling. This kit applies its own semantic tints instead. |
| closeButton | boolean | false | Shows a close button on each toast. |
| duration | number | 4000 | How long a toast stays visible, in milliseconds. |
| width | number | string | 400 | Maximum width of the toast column, not a fixed width: toasts size to their own content and stop here. Set it on the Toaster rather than a single toast, since sonner sizes the list. Ignored below 600px, where toasts go full-bleed. |
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
// @use-when the toast host that renders queued messages.
type MDToasterProps = ToasterProps & {
/** MAXIMUM width of the toast column, in px or any CSS length. Toasts size to
* their own content and stop here; they do not all sit at this width.
*
* Defaults to 400px. A toast should be as small as its message allows, so a
* one-line confirmation stays small and only a toast that has earned the room
* (a description, a row of buttons, a list of secondary actions) grows toward
* the cap. The `toast` item sets the floor, so short toasts never collapse to
* the width of the word inside them. It is a ceiling and not a target: pinning
* every toast to it was tried and reverted, because a Toaster set to 560 then
* rendered a one-line card 560px wide.
*
* It belongs here rather than on an individual toast: sonner sizes the toast
* list itself and positions each toast absolutely inside it, so this value is
* also the containing block every toast is measured against. Below 600px
* sonner ignores it and goes full-bleed on its own. */
width?: number | string
}
function Toaster({ width = 400, style, ...props }: MDToasterProps) {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
/* Bare tokens, NOT the `--color-*` aliases. Those are `@theme inline`
entries declared once at `:root`, and a custom property resolves
where it is DECLARED, so they freeze against the root palette
wherever they are read. That is the bug that made a loading Button
invisible under a scoped palette.
No visible change HERE: a toast portals to <body>, so it sits at
the root anyway and the two spellings agree today. Changed for the
rule, not for a symptom: component source never reads
`var(--color-<semantic>)`, so the next person copying this file
does not carry the trap somewhere it does bite. */
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius-md)",
...(width !== undefined && {
"--width": typeof width === "number" ? `${width}px` : width,
}),
...style,
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }