技术解读
项目背景与痛点
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 只负责“算”,不负责“画”。
另一处设计是把状态显式暴露到 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 端产品,它几乎是默认选项。
生产环境需要注意几点:
- 只挂一个
<Toaster />。它依赖全局单例,微前端或多入口场景下挂多个实例会导致重复渲染,建议在主应用根部统一挂载。 - 不要在渲染期间调用
toast(),应放在事件回调或 effect 里,否则容易触发布局循环。 - 善用
id去重。同一业务动作重复触发时传入固定id,让新消息替换旧消息,而不是堆屏。 - 限制数量。
toast天然缺少“优先级”,高频报错场景建议配合limit或直接改用页面内的持久化错误区域。 - 别承载关键信息。toast 会自动消失,重要结果必须另有落点。
- 自定义主题时优先改 CSS 变量,直接覆盖内部类名会在升级时踩坑。
综合评价
优点:设计判断力强,默认体验在同类库中属于第一梯队;实现上克制而聪明——用 CSS 变量而非 JS 逐帧动画、用 data 属性驱动状态,性能与可定制性兼顾;体积小、无重依赖;无障碍与键盘交互考虑周到。
缺点:opinionated 是双刃剑,想做非标准形态的提示会明显别扭;只服务 React,其它框架只有社区移植;没有 headless 模式,深度定制仍要读源码;单例模型在微前端架构里不够友好;对 CSS 变量与现代浏览器的依赖意味着 IE 时代的老项目无缘。
一句话总结:它不是“功能最全的 toast”,而是“把 toast 该有的样子做对了的那个”。如果你的项目愿意接受它的审美默认值,它能省掉你大量调动画的时间。
评论