ask-sonner

作者: emilkowalski

Sonner(React 弹窗通知库)使用指南——安装并接入 Toaster、选择合适的 toast() 调用、处理 promise 和加载状态的 toast、更新、关闭和持久化 toast、样式、主题和图标、定位以及多个 Toaster。适用于使用 Sonner 或排查相关问题的情况——例如 toast 不显示、重复显示、样式丢失、忽略 Tailwind 类、被模态框遮挡,或不跟随深色模式。

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

Working With Sonner

A guide skill for Sonner, the toast library. When a task involves Sonner — wiring it up, rendering toasts, styling them, or fixing them — answer from this file first. Full prop tables for <Toaster /> and toast() live in API.md; read it when you need an exact prop name, type, or default.

Setup

Two pieces, and only two:

  1. One <Toaster />, mounted once, as close to the root as possible (in Next.js: layout.tsx — it works inside server components). Never render it per-page or conditionally; a second mounted Toaster duplicates every toast.
  2. toast() called from client code — event handlers, effects, callbacks. It's a plain function, no hook or provider needed, but it does nothing on the server: in a server action, return the result and call toast() in the client code that receives it.
import { Toaster } from 'sonner'; // once, in layout
import { toast } from 'sonner';   // anywhere client-side

Picking the right call

You wantCall
Plain messagetoast('Title') — add { description } for a second line
Success / error / info / warning icontoast.success('…'), toast.error('…'), etc.
Spinner while you manage state yourselftoast.loading('…'), then update it by id
Loading → success/error tied to a promisetoast.promise(promise, { loading, success, error }) — success/error accept functions receiving the resolved value/error
Button that does something{ action: { label, onClick } } — closes the toast unless onClick calls event.preventDefault(); cancel is the secondary variant
Custom JSX, default toast shelltoast(<jsx />)
Custom JSX, no styles at alltoast.custom((t) => <jsx />) — headless, t gives you the id to dismiss

Recipes

Update a toast — call toast() again with the same id; only the props you pass change. Switching to toast.success(…, { id }) changes the type. This is how loading → success flows work without toast.promise:

const id = toast.loading('Uploading…');
toast.success('Uploaded', { id });

Persist{ duration: Infinity }. Dismisstoast.dismiss(id), or toast.dismiss() for all. Read active toastsuseSonner() in React, toast.getActiveToasts() outside it.

Links or components in the text — pass a function for the title or description: toast(() => <a href="…">View</a>).

Multiple toasters — give each an id and target with toast('…', { toasterId: 'canvas' }). Without toasterId, every toaster renders the toast.

Close callbacksonDismiss fires on close button or swipe; onAutoClose fires on timeout. They are separate; there is no single "closed" callback.

Styling — the escalation ladder

Climb only as far as the change requires; jumping to the top rung too early is fine (it's the recommended end state), lingering in the middle is not.

  1. Defaults — plus richColors on the Toaster for colorful success/error, invert to flip against the theme.
  2. Inline tweakstoastOptions={{ style: {…} }} on the Toaster for all toasts, or style per toast() call.
  3. Classes on partstoastOptions={{ classNames: { toast, title, description, actionButton, cancelButton, closeButton } }}. Sonner's injected styles win the cascade, so every class needs !important (Tailwind: !text-red-900). If you're marking more than a few things important, stop — go headless.
  4. Headlesstoast.custom() with your own JSX, keeping Sonner's positioning, stacking, and swipe. The recommended approach for a design-system toast: wrap it in your own toast() abstraction. (unstyled: true exists as a halfway house, but headless gives more control for the same effort.)

Icons — swap defaults per-type with the Toaster's icons prop, per-toast with icon, remove with null.

Themetheme defaults to 'light' and does not track the OS. Pass theme="system", or wire your theme provider: <Toaster theme={resolvedTheme} /> from next-themes.

Troubleshooting

SymptomCause → fix
Toast never appearsNo <Toaster /> mounted, or it unmounted (conditional render, per-page placement). Mount one at the root. If calling from a server action: toast() is client-only — call it with the action's result on the client.
Same toast appears twiceTwo Toasters mounted (layout and page) — keep one. Or toast() fired in an effect under React StrictMode's dev double-invoke — fire from the event handler instead, or pass a stable id so the second call updates rather than duplicates.
Tailwind/CSS classes have no effectDefault styles override them. Mark them !important, or use unstyled / headless (see the ladder above).
Toasts render completely unstyled (common in Astro, view transitions)Sonner's injected stylesheet was lost — import it explicitly in a layout: import 'sonner/dist/styles.css'.
Unstyled inside Shadow DOMStyles land in document.head, not the shadow root. Copy the style tag whose text includes [data-sonner-toaster] into the shadow root.
Toast behind a modal/overlay, or clippedAn ancestor creates a stacking context (transform, filter, overflow) or the overlay out-z-indexes the toaster. Move <Toaster /> to the document root, outside any dialog/portal container.
Dark mode ignoredtheme defaults to 'light' — set theme="system" or pass the resolved theme (see Theme above).
Success/error look gray, not green/redThat's the default. Add richColors to the Toaster.
Toast never closesduration: Infinity, dismissible: false, or a toast.promise whose promise never settles — the loading toast waits forever.
toast.promise stuck on loadingIt needs a promise (or a function returning one) as its first argument, and the promise must actually resolve/reject.
Swipe-to-dismiss goes the wrong way / doesn't workDirections derive from position. Override with swipeDirections on the Toaster.
Toast shows up in every toasterMultiple toasters need targeting: give each Toaster an id and pass toasterId in the toast() call.
Toasts too close to the screen edge on mobileoffset (desktop, default 32px) and mobileOffset (<600px, default 16px) — numbers, CSS strings, or per-side objects.

来自 emilkowalski 的更多技能

find-animation-opportunities
emilkowalski
搜索代码库或UI中未添加动画但应当添加动画的位置,并排除所有不应添加动画的内容。只读操作;它提出带有精确数值的动效方案,但不负责实现。当用户询问“这里可以添加什么动画?”或希望“让这个更有活力”时使用。如需修复现有动画,请改用improve-animations或review-animations。
developmentdesigncreative
animate
emilkowalski
从零开始构建动画,按顺序做出决定,以判断动画是否合适——是否应该动画化,目的为何,使用哪种工具,哪些属性,哪种曲线和时长,如何被打断,如何退出。编写实现代码。当被要求为某物添加动画、增加动态效果、让组件有生命力,或构建过渡效果时使用。若要批评现有动态效果,使用review-animations;若要审计整个代码库,使用improve-animations。
pick-ui-library
emilkowalski
从前端任务的精选列表中选择合适的库——数字输入、OTP输入、图表、命令菜单、虚拟化、拖放、提示通知、状态管理、样式等。仅在明确调用时运行,不会自行触发。
prototype
emilkowalski
为你描述的UI组件构建多个真正不同的版本,渲染在可视化选择器后方,便于你实时切换浏览并提升最合适的一个。仅在显式调用时运行;不会自行触发。
developmentdesigncreative
emil-design-eng
emilkowalski
该技能编码了Emil Kowalski关于UI打磨、组件设计、动画决策以及那些让软件体验出色的隐形细节的设计哲学。
designdevelopmentcreative
review-animations
emilkowalski
根据Emil Kowalski的设计工程哲学,对动画和动效代码进行高工艺标准审查。默认标记问题,需争取通过。
animation-vocabulary
emilkowalski
反向查找词汇表,将关于网页动画或运动效果的模糊描述转化为精确术语(例如:“弹出窗口打开时的弹跳效果” → 弹入;“iOS的橡皮筋滚动” → 橡皮筋回弹)。当用户问“那个叫什么来着……”,或描述一个运动效果但
creativedesignresearch
improve-animations
emilkowalski
以资深动效顾问的身份审查代码库中的动画与动效代码,生成一份优先级排序的审计报告及自包含的实现方案,供其他智能体(或更经济的模型)执行。仅对源代码进行只读操作——它规划改进方案,但不实际应用。适用于用户提出“改进动画效果”、“审计动效”、“让这个应用体验更好”,或希望获得动画修复路线图而非单一差异审查的场景。