MIT License

技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

项目背景与痛点

Toast 提示大概是前端组件里最“看起来简单、做起来很麻烦”的一个。它需要满足这些约束:能随时调用而不用把状态层层传递、动画必须顺滑、堆叠时能看出前后层级、高度不确定时布局不能崩、还要处理键盘可达性与暗色主题。

社区里从不缺 toast 库:react-hot-toast 轻量但样式朴素,react-toastify 功能齐全但 API 繁琐、视觉偏“企业后台”。更关键的是,绝大多数库在处理“多条消息同时出现”时选择回避——要么纵向全部铺开,要么简单地叠在一起,牺牲了动效和层级关系。

sonner 由 Emil Kowalski(也是 vaul 抽屉组件的作者)开发。它的定位很明确:对样式有主见的 toast,默认就好看,把动画和堆叠这类容易做糟的细节直接做对。它不追求配置项数量,而是把“消息出现的瞬间质感”当作产品本身。

核心技术揭秘

sonner 最值得注意的,是它如何解决命令式调用与 React 声明式渲染之间的矛盾,以及高度不定的堆叠动画如何处理。

它内部维护一个纯 JavaScript 的 ToastState 单例:toast() 可以脱离 React 上下文调用(比如写在 axios 拦截器、路由守卫里),只负责创建记录、写入队列、通知订阅者;真正挂载这个单例的是根部的 <Toaster /> 组件,它通过订阅回调触发重渲染。这与 useSyncExternalStore 的思路一致,但用了更轻的实现。

难点在布局。每条 toast 的高度取决于内容行数、是否有操作按钮,堆叠效果又要求后面的 toast 按前面的高度依次偏移并缩放。sonner 的做法是:在 useLayoutEffect 中测量每条 toast 的真实高度并缓存,再计算索引、偏移量、抬升高度,把这些数值写进 CSS 变量,由 CSS 完成 transform 与 transition。JS 只负责“算”,不负责“画”。

graph TD A[业务代码调用 toast 函数] --> B[ToastState 单例:订阅中心] B --> C[通知订阅者:Toaster 组件] C --> D[创建 toast 记录并入队] D --> E[渲染层挂载 DOM 节点] E --> F[useLayoutEffect 测量真实高度] F --> G[计算堆叠偏移、缩放与层级] G --> H[把结果写入 CSS 变量] H --> I[CSS transition 驱动动画] E --> J{"toast.promise 调用"} J -->|"是"| K[loading 到 success 或 error 状态切换] J -->|"否"| L[按类型渲染图标与内容]

另一处设计是把状态显式暴露到 DOM 上:data-mounted、data-visible、data-expanded、data-front、data-removed。所有视觉变化都由这些属性配合 CSS 选择器完成,而不是在 JS 里逐帧计算。这带来两个好处:动画天然可被打断、可被用户的 CSS 覆盖;同时绕开了对 framer-motion 这类动画库的依赖,包体积保持在很小的量级。

交互层面同样是“少而准”:悬停时堆叠自动展开,指针拖拽按方向与位移阈值判定滑动移除,Alt+T 聚焦最新一条 toast 并进入 Tab 序列,容器附带 aria-live,确保屏幕阅读器能播报。

功能亮点与差异

  • 默认动效的方向感:toast 从哪个角落滑出、往哪个方向消失,与所选位置绑定,而不是千篇一律的淡入淡出。
  • 真正的堆叠视觉:后方 toast 缩放并露出边缘,悬停展开,这种“卡片垛”的观感是同类库里少有的。
  • toast.promise 的状态机映射:一个 Promise 自动对应 loading → success/error 三段文案,把“请求中”这种最常见的场景压缩成一行代码。
  • 样式定制的正确做法:通过 CSS 变量(背景、边框、文字色)与 classNames 槽位覆盖,而不是塞一堆 style 对象属性。
  • 零动画库依赖:不引入重型依赖,体积对首屏友好。

相比竞品,它的差异其实不在功能多少,而在默认值的质量。绝大多数库把“好看”留给使用者,sonner 把“好看”当作默认交付物。

应用场景与落地建议

适合的场景很清晰:产品型 Web 应用、SaaS 后台、需要频繁反馈的交互流程(保存、复制、提交、上传)。如果你是重设计感的 C 端产品,它几乎是默认选项。

生产环境需要注意几点:

  1. 只挂一个 <Toaster />。它依赖全局单例,微前端或多入口场景下挂多个实例会导致重复渲染,建议在主应用根部统一挂载。
  2. 不要在渲染期间调用 toast(),应放在事件回调或 effect 里,否则容易触发布局循环。
  3. 善用 id 去重。同一业务动作重复触发时传入固定 id,让新消息替换旧消息,而不是堆屏。
  4. 限制数量。toast 天然缺少“优先级”,高频报错场景建议配合 limit 或直接改用页面内的持久化错误区域。
  5. 别承载关键信息。toast 会自动消失,重要结果必须另有落点。
  6. 自定义主题时优先改 CSS 变量,直接覆盖内部类名会在升级时踩坑。

综合评价

优点:设计判断力强,默认体验在同类库中属于第一梯队;实现上克制而聪明——用 CSS 变量而非 JS 逐帧动画、用 data 属性驱动状态,性能与可定制性兼顾;体积小、无重依赖;无障碍与键盘交互考虑周到。

缺点:opinionated 是双刃剑,想做非标准形态的提示会明显别扭;只服务 React,其它框架只有社区移植;没有 headless 模式,深度定制仍要读源码;单例模型在微前端架构里不够友好;对 CSS 变量与现代浏览器的依赖意味着 IE 时代的老项目无缘。

一句话总结:它不是“功能最全的 toast”,而是“把 toast 该有的样子做对了的那个”。如果你的项目愿意接受它的审美默认值,它能省掉你大量调动画的时间。

关于

GitHub 原始描述(英文)

An opinionated toast component for React.

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

简要分析

热度分
14106
价值分
8027
活跃状态
活跃

语言占比

CSS
JavaScript
TypeScript

Release

README

https://github.com/vallezw/sonner/assets/50796600/59b95cb7-9068-4f3e-8469-0b35d9de5cf0

Sonner is an opinionated toast component for React. You can read more about why and how it was built here.

Usage

To start using the library, install it in your project:

npm install sonner

Alternatively you can use the /ask-sonner skill to do it for you and help you with any other Sonner-related questions.

npx skills add https://github.com/emilkowalski/skills --skill ask-sonner

Add <Toaster /> to your app, it will be the place where all your toasts will be rendered. After that you can use toast() from anywhere in your app.

import { Toaster, toast } from 'sonner';

// ...

function App() {
  return (
    <div>
      <Toaster />
      <button => toast('My first toast')}>Give me a toast</button>
    </div>
  );
}

Documentation

Find the full API reference in the documentation.

评论

暂无评论