feat(components): changed sonner defaults to use lucide icons (#7620)

* feat(components): changed sonner defaults to use lucide icons

* Update new-york-v4 sonner.tsx

* fix: icons and docs

* fix

* fix

---------

Co-authored-by: shadcn <m@shadcn.com>
This commit is contained in:
Jakob Guddas
2025-10-13 20:16:50 +02:00
committed by GitHub
parent 41f4f7357d
commit 77bf7d28b4
17 changed files with 250 additions and 10 deletions

View File

@@ -68,7 +68,7 @@ npm install sonner next-themes
<Step>Add the Toaster component</Step>
```tsx title="app/layout.tsx" {1,9}
```tsx showLineNumbers title="app/layout.tsx" {1,8}
import { Toaster } from "@/components/ui/sonner"
export default function RootLayout({ children }) {
@@ -76,8 +76,8 @@ export default function RootLayout({ children }) {
<html lang="en">
<head />
<body>
<main>{children}</main>
<Toaster />
<main>{children}</main>
</body>
</html>
)
@@ -99,3 +99,56 @@ import { toast } from "sonner"
```tsx
toast("Event has been created.")
```
## Examples
<ComponentPreview name="sonner-types" />
## Changelog
### 2025-10-13 Icons
We've updated the Sonner component to use icons from `lucide`. Update your `sonner.tsx` file to use the new icons.
```tsx showLineNumbers title="components/ui/sonner.tsx" {3-9,20-26}
"use client"
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }
```

View File

@@ -10,7 +10,7 @@
"files": [
{
"path": "ui/sonner.tsx",
"content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n toastOptions={{\n classNames: {\n toast:\n \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n description: \"group-[.toast]:text-muted-foreground\",\n actionButton:\n \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n cancelButton:\n \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n },\n }}\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
"content": "\"use client\"\n\nimport {\n CircleCheck,\n Info,\n LoaderCircle,\n OctagonX,\n TriangleAlert,\n} from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n icons={{\n success: <CircleCheck className=\"h-4 w-4\" />,\n info: <Info className=\"h-4 w-4\" />,\n warning: <TriangleAlert className=\"h-4 w-4\" />,\n error: <OctagonX className=\"h-4 w-4\" />,\n loading: <LoaderCircle className=\"h-4 w-4 animate-spin\" />,\n }}\n toastOptions={{\n classNames: {\n toast:\n \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n description: \"group-[.toast]:text-muted-foreground\",\n actionButton:\n \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n cancelButton:\n \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n },\n }}\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
"type": "registry:ui",
"target": ""
}

View File

@@ -6863,6 +6863,19 @@
}
]
},
{
"name": "sonner-types",
"type": "registry:example",
"registryDependencies": [
"sonner"
],
"files": [
{
"path": "registry/new-york-v4/examples/sonner-types.tsx",
"type": "registry:example"
}
]
},
{
"name": "spinner-demo",
"type": "registry:example",

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "sonner-types",
"type": "registry:example",
"registryDependencies": [
"sonner"
],
"files": [
{
"path": "registry/new-york-v4/examples/sonner-types.tsx",
"content": "\"use client\"\n\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\n\nexport default function SonnerTypes() {\n return (\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" onClick={() => toast(\"Event has been created\")}>\n Default\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => toast.success(\"Event has been created\")}\n >\n Success\n </Button>\n <Button\n variant=\"outline\"\n onClick={() =>\n toast.info(\"Be at the area 10 minutes before the event time\")\n }\n >\n Info\n </Button>\n <Button\n variant=\"outline\"\n onClick={() =>\n toast.warning(\"Event start time cannot be earlier than 8am\")\n }\n >\n Warning\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => toast.error(\"Event has not been created\")}\n >\n Error\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => {\n const promise = () =>\n new Promise((resolve) =>\n setTimeout(() => resolve({ name: \"Event\" }), 2000)\n )\n\n toast.promise(promise(), {\n loading: \"Loading...\",\n success: (data) => `${data.name} has been created`,\n error: \"Error\",\n })\n }}\n >\n Promise\n </Button>\n </div>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -9,7 +9,7 @@
"files": [
{
"path": "registry/new-york-v4/ui/sonner.tsx",
"content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner, ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\",\n \"--border-radius\": \"var(--radius)\",\n } as React.CSSProperties\n }\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
"content": "\"use client\"\n\nimport {\n CircleCheckIcon,\n InfoIcon,\n Loader2Icon,\n OctagonXIcon,\n TriangleAlertIcon,\n} from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner, ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n icons={{\n success: <CircleCheckIcon className=\"size-4\" />,\n info: <InfoIcon className=\"size-4\" />,\n warning: <TriangleAlertIcon className=\"size-4\" />,\n error: <OctagonXIcon className=\"size-4\" />,\n loading: <Loader2Icon className=\"size-4 animate-spin\" />,\n }}\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\",\n \"--border-radius\": \"var(--radius)\",\n } as React.CSSProperties\n }\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
"type": "registry:ui"
}
]

View File

@@ -6863,6 +6863,19 @@
}
]
},
{
"name": "sonner-types",
"type": "registry:example",
"registryDependencies": [
"sonner"
],
"files": [
{
"path": "registry/new-york-v4/examples/sonner-types.tsx",
"type": "registry:example"
}
]
},
{
"name": "spinner-demo",
"type": "registry:example",

View File

@@ -6956,6 +6956,24 @@ export const Index: Record<string, any> = {
categories: undefined,
meta: undefined,
},
"sonner-types": {
name: "sonner-types",
description: "",
type: "registry:example",
registryDependencies: ["sonner"],
files: [{
path: "registry/new-york-v4/examples/sonner-types.tsx",
type: "registry:example",
target: ""
}],
component: React.lazy(async () => {
const mod = await import("@/registry/new-york-v4/examples/sonner-types.tsx")
const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name
return { default: mod.default || mod[exportName] }
}),
categories: undefined,
meta: undefined,
},
"spinner-demo": {
name: "spinner-demo",
description: "",

View File

@@ -0,0 +1,61 @@
"use client"
import { toast } from "sonner"
import { Button } from "@/registry/new-york-v4/ui/button"
export default function SonnerTypes() {
return (
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={() => toast("Event has been created")}>
Default
</Button>
<Button
variant="outline"
onClick={() => toast.success("Event has been created")}
>
Success
</Button>
<Button
variant="outline"
onClick={() =>
toast.info("Be at the area 10 minutes before the event time")
}
>
Info
</Button>
<Button
variant="outline"
onClick={() =>
toast.warning("Event start time cannot be earlier than 8am")
}
>
Warning
</Button>
<Button
variant="outline"
onClick={() => toast.error("Event has not been created")}
>
Error
</Button>
<Button
variant="outline"
onClick={() => {
toast.promise<{ name: string }>(
() =>
new Promise((resolve) =>
setTimeout(() => resolve({ name: "Event" }), 2000)
),
{
loading: "Loading...",
success: (data) => `${data.name} has been created`,
error: "Error",
}
)
}}
>
Promise
</Button>
</div>
)
}

View File

@@ -1,5 +1,12 @@
"use client"
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
@@ -10,6 +17,13 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",

View File

@@ -2016,6 +2016,17 @@ export const examples: Registry["items"] = [
},
],
},
{
name: "sonner-types",
type: "registry:example",
registryDependencies: ["sonner"],
files: [
{
path: "examples/sonner-types.tsx",
type: "registry:example",
},
],
},
{
name: "spinner-demo",
type: "registry:example",

View File

@@ -10,7 +10,7 @@
"files": [
{
"path": "ui/sonner.tsx",
"content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n toastOptions={{\n classNames: {\n toast:\n \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n description: \"group-[.toast]:text-muted-foreground\",\n actionButton:\n \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n cancelButton:\n \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n },\n }}\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
"content": "\"use client\"\n\nimport {\n CircleCheck,\n Info,\n LoaderCircle,\n OctagonX,\n TriangleAlert,\n} from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n icons={{\n success: <CircleCheck className=\"h-4 w-4\" />,\n info: <Info className=\"h-4 w-4\" />,\n warning: <TriangleAlert className=\"h-4 w-4\" />,\n error: <OctagonX className=\"h-4 w-4\" />,\n loading: <LoaderCircle className=\"h-4 w-4 animate-spin\" />,\n }}\n toastOptions={{\n classNames: {\n toast:\n \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n description: \"group-[.toast]:text-muted-foreground\",\n actionButton:\n \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n cancelButton:\n \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n },\n }}\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
"type": "registry:ui",
"target": ""
}

View File

@@ -6863,6 +6863,19 @@
}
]
},
{
"name": "sonner-types",
"type": "registry:example",
"registryDependencies": [
"sonner"
],
"files": [
{
"path": "registry/new-york-v4/examples/sonner-types.tsx",
"type": "registry:example"
}
]
},
{
"name": "spinner-demo",
"type": "registry:example",

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "sonner-types",
"type": "registry:example",
"registryDependencies": [
"sonner"
],
"files": [
{
"path": "registry/new-york-v4/examples/sonner-types.tsx",
"content": "\"use client\"\n\nimport { toast } from \"sonner\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\n\nexport default function SonnerTypes() {\n return (\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" onClick={() => toast(\"Event has been created\")}>\n Default\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => toast.success(\"Event has been created\")}\n >\n Success\n </Button>\n <Button\n variant=\"outline\"\n onClick={() =>\n toast.info(\"Be at the area 10 minutes before the event time\")\n }\n >\n Info\n </Button>\n <Button\n variant=\"outline\"\n onClick={() =>\n toast.warning(\"Event start time cannot be earlier than 8am\")\n }\n >\n Warning\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => toast.error(\"Event has not been created\")}\n >\n Error\n </Button>\n <Button\n variant=\"outline\"\n onClick={() => {\n const promise = () =>\n new Promise((resolve) =>\n setTimeout(() => resolve({ name: \"Event\" }), 2000)\n )\n\n toast.promise(promise(), {\n loading: \"Loading...\",\n success: (data) => `${data.name} has been created`,\n error: \"Error\",\n })\n }}\n >\n Promise\n </Button>\n </div>\n )\n}\n",
"type": "registry:example"
}
]
}

View File

@@ -9,7 +9,7 @@
"files": [
{
"path": "registry/new-york-v4/ui/sonner.tsx",
"content": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner, ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\",\n \"--border-radius\": \"var(--radius)\",\n } as React.CSSProperties\n }\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
"content": "\"use client\"\n\nimport {\n CircleCheckIcon,\n InfoIcon,\n Loader2Icon,\n OctagonXIcon,\n TriangleAlertIcon,\n} from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner, ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n icons={{\n success: <CircleCheckIcon className=\"size-4\" />,\n info: <InfoIcon className=\"size-4\" />,\n warning: <TriangleAlertIcon className=\"size-4\" />,\n error: <OctagonXIcon className=\"size-4\" />,\n loading: <Loader2Icon className=\"size-4 animate-spin\" />,\n }}\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\",\n \"--border-radius\": \"var(--radius)\",\n } as React.CSSProperties\n }\n {...props}\n />\n )\n}\n\nexport { Toaster }\n",
"type": "registry:ui"
}
]

View File

@@ -24,7 +24,7 @@ type FormFieldContextValue<
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
@@ -70,7 +70,7 @@ type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue| null>(null)
const FormItemContext = React.createContext<FormItemContextValue | null>(null)
const FormItem = React.forwardRef<
HTMLDivElement,

View File

@@ -1,5 +1,12 @@
"use client"
import {
CircleCheck,
Info,
LoaderCircle,
OctagonX,
TriangleAlert,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
@@ -12,6 +19,13 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheck className="h-4 w-4" />,
info: <Info className="h-4 w-4" />,
warning: <TriangleAlert className="h-4 w-4" />,
error: <OctagonX className="h-4 w-4" />,
loading: <LoaderCircle className="h-4 w-4 animate-spin" />,
}}
toastOptions={{
classNames: {
toast:

View File

@@ -24,7 +24,7 @@ type FormFieldContextValue<
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
@@ -70,7 +70,7 @@ type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue| null>(null)
const FormItemContext = React.createContext<FormItemContextValue | null>(null)
const FormItem = React.forwardRef<
HTMLDivElement,