Sonner
轻提示:浮层面板,左边 32px 的淡色类型方块,粗体标题加一行弱色说明,右端是安静的关闭键。
sonner-demo.tsx
"use client"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Toaster } from "@/components/ui/sonner"
function publish() {
return new Promise<{ title: string }>((resolve) =>
setTimeout(() => resolve({ title: "葬送的芙莉莲 第二季 - 09" }), 1600)
)
}
export default function SonnerDemo() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
{/* Mount one Toaster near the root of your app; it is here so the demo is self-contained. */}
<Toaster position="bottom-right" />
<Button
variant="secondary"
onClick={() => toast.success("已复制磁力链接", { description: "粘贴到下载工具即可开始" })}
>
成功
</Button>
<Button
variant="secondary"
onClick={() => toast.info("今晚 23:30 更新", { description: "间谍过家家 第三季 · 第 4 集" })}
>
信息
</Button>
<Button
variant="secondary"
onClick={() => toast.warning("RSS 密钥即将过期", { description: "还剩 3 天,可在设置里续期" })}
>
警告
</Button>
<Button
variant="secondary"
onClick={() => toast.error("发布失败", { description: "种子文件的 info hash 已经存在" })}
>
失败
</Button>
<Button
variant="secondary"
onClick={() =>
toast("已删除 1 条资源", {
description: "药屋少女的呢喃 第三季 - 12",
action: { label: "撤销", onClick: () => toast.success("已恢复") },
})
}
>
带操作
</Button>
<Button
onClick={() =>
toast.promise(publish(), {
loading: "正在发布…",
success: (release) => ({ message: "发布成功", description: release.title }),
error: "发布失败",
})
}
>
发布(进行中)
</Button>
</div>
)
}安装
npx shadcn@latest add @anibt/sonner第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。
手动安装
- 1. 先装主题组件只读
@anibt/theme的 token 和配方,没有主题就没有样子。npx shadcn@latest add @anibt/theme - 2. 安装依赖
npm install sonner next-themes lucide-react - 3. 把下面的文件放进项目components/ui/sonner.tsx
"use client" import * as React from "react" import { CheckIcon, InfoIcon, Loader2Icon, TriangleAlertIcon, XIcon } from "lucide-react" import { useTheme } from "next-themes" import { Toaster as Sonner, type ToasterProps } from "sonner" /** * Toaster — AniBT 「贴纸手账」 toasts on sonner. * * Every toast is the floating panel: paper, a 1px edge and the pop shadow on * radius 14, 12px of padding. The type is a 32px tinted tile in the icon slot * — mint for done, blue for info, amber for a warning, red for a failure, the * sunk well with a spinner for work in flight — beside a 13/700 title and a * 12px muted line. The words still say the state on their own. The close * button is a quiet 24px square at the right end; an action is a small raised * key, a cancel is quiet. * * sonner styles a toast from attribute selectors, which outrank a utility * class, so the overrides here are `!important`. Its own 1px border is * dropped and the edge drawn in the shadow, so a toast has one line. * * Mount one `<Toaster />` near the root (shadcn's export name), then call * `toast.success("已复制磁力链接", { description: "…" })` from `sonner`. */ const TILE = "grid size-8 place-items-center rounded-[10px] [&>svg]:size-4" const Toaster = ({ ...props }: ToasterProps) => { const { theme = "system" } = useTheme() return ( <Sonner theme={theme as ToasterProps["theme"]} className="toaster group" closeButton gap={10} icons={{ success: ( <span className={`${TILE} bg-anibt-ok-wash text-anibt-ok`}> <CheckIcon strokeWidth={2.75} /> </span> ), info: ( <span className={`${TILE} bg-anibt-time-wash text-anibt-time-ink`}> <InfoIcon /> </span> ), warning: ( <span className={`${TILE} bg-anibt-warn-wash text-anibt-warn`}> <TriangleAlertIcon /> </span> ), error: ( <span className={`${TILE} bg-anibt-danger-wash text-destructive`}> <XIcon strokeWidth={2.75} /> </span> ), loading: ( <span className={`${TILE} bg-muted text-muted-foreground shadow-[inset_0_0_0_1px_var(--border)]`}> <Loader2Icon className="animate-spin" /> </span> ), }} toastOptions={{ classNames: { toast: "anibt-pop! border-0! gap-3! p-3! pr-11! text-[13px]! font-sans! focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid", title: "font-bold! text-[13px]! leading-snug! text-popover-foreground!", description: "text-[12px]! leading-normal! text-muted-foreground!", icon: "size-8! m-0! shrink-0!", closeButton: "top-1/2! size-6! rounded-[6px]! border-0! bg-transparent! text-muted-foreground! shadow-none! hover:bg-accent! hover:text-accent-foreground! focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid [&>svg]:size-3.5!", actionButton: "anibt-btn-raised! h-7! rounded-[10px]! px-3! text-[12px]! font-semibold!", cancelButton: "h-7! rounded-[10px]! bg-transparent! px-3! text-[12px]! font-semibold! text-anibt-ink2! hover:bg-accent! hover:text-accent-foreground!", }, }} style={ { "--normal-bg": "var(--popover)", "--normal-text": "var(--popover-foreground)", "--normal-border": "var(--border)", "--border-radius": "14px", // The close button sits at the right end, centred, instead of on the // top-left corner. "--toast-close-button-start": "auto", "--toast-close-button-end": "8px", "--toast-close-button-transform": "translateY(-50%)", // sonner's internal greys, pointed at the theme. "--gray2": "var(--muted)", "--gray4": "var(--border)", "--gray5": "var(--border)", "--gray11": "var(--muted-foreground)", "--gray12": "var(--popover-foreground)", } as React.CSSProperties } {...props} /> ) } export { Toaster }
用法
在应用根部放一个 <Toaster />(只放一个,否则每条提示会出现两次),然后在任何地方调用 sonner 的 toast:
app/layout.tsx
import { Toaster } from "@/components/ui/sonner"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
{children}
<Toaster />
</body>
</html>
)
}components/copy-magnet.tsx
import { toast } from "sonner"
toast.success("已复制磁力链接", { description: "粘贴到下载工具即可开始" })类型
toast.success:薄荷色方块和勾。toast.info:蓝色方块。toast.warning:琥珀色方块和警示三角。toast.error:红色方块和叉。toast.loading/toast.promise:下沉的灰色方块里转圈,结束后换成成功或失败。toast(...):没有方块的普通提示。
颜色只是辅助,标题本身要说清楚发生了什么。
操作
action 是一个小号凸起键,cancel 是安静键,适合「撤销」这类一步可回的操作。
带撤销
toast("已删除 1 条资源", {
description: "药屋少女的呢喃 第三季 - 12",
action: { label: "撤销", onClick: restore },
})主题
Toaster 从 next-themes 读取当前主题,没有 ThemeProvider 时跟随系统。面板颜色全部来自 @anibt/theme 的 token,白樱和夜樱各自正确。
属性
Toaster 接受 sonner 的全部属性(position、expand、duration、richColors……),默认打开 closeButton。
