懶人包
- ask-sonner 是 Sonner 作者 Emil Kowalski 寫的 AI agent skill,收在
emilkowalski/skills(MIT),2026-08-10 加入。內容是兩個檔:88 行的SKILL.md(設定、選用哪種 toast、樣式、疑難排解)和 64 行的API.md(props 表)。Sonner 自己的 README 也推薦用它來裝。- Sonner 本身很紅:npm 最新版 2.0.8,2026-09-19 到 10-18 這 30 天下載 2.14 億次。skill repo 在 2026-10-21 是 48,159 stars,這是 15 支 skill 共用的數字。
- 我拿它對照 Sonner 2.0.8 的型別檔,找到一個會讓程式壞掉的錯:skill 教「在 React 外讀取 toast 用
toast.getActiveToasts()」,但 2.0.8 公開的是toast.getToasts()。另外offset預設值寫 32px,原始碼是 24px。- 我用同一個需求讓 Claude Code 跑 5 組:唯一
npm run build失敗的,就是載入了 skill、照抄getActiveToasts的那一組。點名/ask-sonner那組有自己去查型別、改用正確 API,也照 skill 用了更精簡的toast.promise寫法。- 沒點名的情況跑 2 次只觸發 1 次。建議點名使用,並且讓 AI 寫完一定跑一次
tsc。
- ask-sonner 是什麼?
- Sonner 是什麼、有多紅?
- 什麼時候會被觸發?
- API.md 跟 Sonner 2.0.8 對得上嗎?
- 實測:有 skill 跟沒 skill,寫出來差在哪?
- 怎麼安裝、怎麼點名?
- 適合誰?限制與坑
- 延伸資源
🍞 ask-sonner 是什麼?
ask-sonner 是一份給 AI coding agent 讀的 Sonner 使用手冊。Sonner 是 React 的 toast(右下角跳出來的通知)函式庫,作者跟這支 skill 是同一個人:Emil Kowalski。
它跟同 repo 其他 skill 不一樣。其他支多半在講動效原則或設計判斷,ask-sonner 只做一件事:讓 AI 在碰到 Sonner 時,先照作者本人的說法回答,不要靠訓練資料裡的舊印象亂寫。SKILL.md 裡寫的是「When a task involves Sonner — wiring it up, rendering toasts, styling them, or fixing them — answer from this file first」。
內容分成兩個檔:
| 檔案 | 行數 | 內容 |
|---|---|---|
SKILL.md | 88 | 設定兩原則、該用哪種 toast() 的對照表、常用寫法(以 id 更新、持久、關閉、多個 toaster、關閉回呼)、樣式的四階升級、主題、13 列疑難排解表 |
API.md | 64 | <Toaster /> props、toast() options、函式清單,含型別與預設值 |
SKILL.md 裡我覺得最有用的是兩段。第一段是樣式的「升級梯子」:先用預設加 richColors,不夠再用 style,再不夠才用 classNames(而且每個 class 都要 !important,因為 Sonner 自帶的樣式會贏),標了太多 !important 就直接改用 toast.custom() 自己畫。第二段是疑難排解表,把「toast 不出現」「出現兩次」「Tailwind class 沒效果」「被 modal 蓋住」「深色模式沒跟上」這些症狀直接對到原因。
Sonner 官方 README 在安裝段落寫著:「Alternatively you can use the /ask-sonner skill to do it for you」,所以它算是作者推薦的入口。
📈 Sonner 是什麼、有多紅?
我在 2026-10-21 查了 npm registry 和 GitHub API:
| 項目 | 數字 |
|---|---|
| npm 最新版 | 2.0.8(2026-08-09 發布,上一版 2.0.7 是 2025-08-02) |
| npm 下載量 | 214,265,841 次(2026-09-19~10-18,npm last-month API) |
emilkowalski/sonner repo | 13,059 stars、468 forks |
emilkowalski/skills repo | 48,159 stars、2,785 forks,最後 push 2026-10-11 |
emilkowalski/skills 裡一共有 15 支 skill,GitHub 只能對整個 repo 加星,所以 48k 是整套共用的數字,不是 ask-sonner 一支的。
ask-sonner 加入 repo 的 commit 是 2026-08-10,跟 Sonner 2.0.8 發布只差一天。
🎯 什麼時候會被觸發?
SKILL.md 的 frontmatter 只有 name 和 description,沒有 disable-model-invocation,所以 Claude Code 可以自己判斷要不要載入它。description 寫的觸發情境很具體:裝 Toaster、選 toast() 呼叫、promise/loading toast、更新與關閉、樣式、主題、圖示、多個 toaster,以及「toast 不出現、出現兩次、樣式不見、Tailwind 沒效、被 modal 蓋住、不跟深色模式」這幾種問題。
另一個要知道的設定是「Initial Response」:如果你只打 /ask-sonner、後面沒帶問題,它被要求只回一句:
I'm ready to help you set up, style, and troubleshoot Sonner, my knowledge comes from its author, Emil Kowalski.
然後等你提問。所以點名時要把需求接在後面一起送,不要只打指令。
🔍 API.md 跟 Sonner 2.0.8 對得上嗎?
不完全對得上。我把 API.md 逐列跟 npm 上 sonner@2.0.8 的 dist/index.d.ts 和 dist/index.mjs 對照,大部分預設值一致(visibleToasts 3、gap 14、duration 4000、position bottom-right、theme light、mobileOffset 16px),但有幾處不一樣:
| 項目 | skill 寫的 | 2.0.8 實際 | 影響 |
|---|---|---|---|
| 在 React 外讀取 toast | toast.getActiveToasts() | 公開 API 只有 toast.getToasts() 和 toast.getHistory();getActiveToasts 是內部 ToastState 的方法,getToasts 裡面呼叫它 | 照寫會 TypeScript 編譯失敗,執行時 TypeError |
offset 預設 | '32px' | 原始碼 const VIEWPORT_OFFSET = '24px' | 只影響你對間距的預期 |
containerAriaLabel | 列在 toast() options | 是 <Toaster /> 的 prop,預設 'Notifications' | 放錯地方會被型別擋下 |
hotkey 型別 | string | string[],預設 ['altKey', 'KeyT'] | 照字串寫會型別錯誤 |
toastOptions | 「任何 toast() option」 | 型別只開放一部分(className、style、duration、classNames、closeButton、unstyled、toasterId 等) | 例如 action 不能放在這裡設成全域預設 |
API.md 沒列、但 2.0.8 有的:<Toaster /> 的 duration、className、style、customAriaLabel;toast() 的 richColors、className、descriptionClassName;classNames 除了 skill 列的六個部位,還能指定 icon、content、loader 和各 type(success、error…);toast.promise 的設定還有 description、finally,回傳值帶 unwrap()。
getActiveToasts 這個錯不是 skill 自己發明的。我查了 Sonner 官方文件站的 Other 頁面,也是寫 toast.getActiveToasts();GitHub main 分支的 src/state.ts 匯出的一樣是 getHistory 和 getToasts。看起來是官方文件與程式碼不同步,skill 照著文件寫,於是一起錯。
🧪 實測:有 skill 跟沒 skill,寫出來差在哪?
我用 Vite + React + TypeScript 建了一個基底專案,裝 sonner@2.0.8,複製成 5 份,用同一句需求跑 claude -p --dangerously-skip-permissions(Claude Code 2.1.306,模型 Sonnet)。需求是:
1. 一顆「上傳」按鈕,呼叫 1.5 秒後隨機成功或失敗的假上傳函式(成功時回傳檔名),
用 toast 顯示「上傳中」→「成功/失敗」,成功訊息要帶出回傳的檔名。
2. 成功的 toast 要有「復原」按鈕,點了之後再跳一個「已復原」的 toast。
3. toast 要跟著作業系統的深色模式切換,成功和失敗要有顏色。
4. 在 src/uploadApi.ts(不是 React 元件的一般模組)提供 countVisibleToasts(),
回傳目前畫面上有幾個 toast;App 裡加一顆按鈕把這個數字 alert 出來。
不要安裝其他套件。寫完檔案就結束,不用解釋。
第 4 點是我刻意設計的:它對應 skill 裡「在 React 外讀取 toast」那一條,正好是 API.md 跟 2.0.8 對不上的地方。第 3 點對應 skill 的 Theme 段落(預設 light、不跟 OS)。
5 組的設定與結果:
| 組別 | 設定 | skill 有沒有用上 | countVisibleToasts 寫法 | npm run build |
|---|---|---|---|---|
| auto1 | 裝 skill、不點名 | 沒有呼叫 Skill | toast.getToasts().length | ✅ 通過 |
| auto2 | 裝 skill、不點名 | 有呼叫 Skill | toast.getActiveToasts().length | ❌ TS2339 |
| named1 | 需求前加 /ask-sonner | 有(內容直接注入) | toast.getToasts() 再濾掉已關閉的 | ✅ 通過 |
| without1 | 沒裝 skill | — | toast.getToasts().length | ✅ 通過 |
| without2 | 沒裝 skill | — | toast.getToasts() 再濾掉已關閉的 | ✅ 通過 |
tsc -b && vite build。auto2 是唯一失敗的一組:
我另外把 auto2 只跑 vite build(跳過型別檢查)再開起來,用 Playwright 點「顯示 toast 數量」,頁面丟出 ue.getActiveToasts is not a function。也就是說,如果專案的 build 不跑 tsc,這個錯會一路帶到執行時才爆。
我翻了每組的 stream-json 紀錄,看它們是怎麼寫出那一行的:
- auto2:載入 skill 後直接寫檔,過程只有一次 Glob 和兩次 Write,沒有去看
node_modules,也沒有自己跑建置。skill 寫什麼它就用什麼。 - named1:skill 內容注入後,它還是去 grep 了
index.d.ts,發現沒有getActiveToasts,改用getToasts,自己跑tsc -b通過,最後的訊息寫明「已安裝的 sonner 是 2.0.8,沒有 getActiveToasts」。 - auto1、without1、without2:都去讀了
node_modules/sonner的型別或原始碼,自己找到getToasts。
skill 也有幫上忙的地方。點名的 named1 是 5 組裡唯一用 toast.promise 把「上傳中 → 成功 → 帶復原按鈕」寫成一個呼叫的:
toast.promise(fakeUpload(), {
loading: '上傳中…',
success: (fileName) => ({
message: `上傳成功:${fileName}`,
action: {
label: '復原',
onClick: () => toast('已復原'),
},
}),
error: '上傳失敗',
})
這對應 API.md 對 toast.promise 的說明:「success/error accept strings, JSX, functions of the result, or objects of toast options」。其他 4 組(包括 auto2)都是先 toast.loading() 拿 id,再用 toast.success(..., { id }) 更新,也能動,只是多寫幾行。
深色模式那一題 5 組都寫了 <Toaster theme="system" richColors />,有沒有 skill 沒差。
下面是 named1 的實際畫面,用 Playwright 分別以淺色、深色模式開啟(為了截到成功的那一刻,我把 Math.random 固定成會成功)。左邊是成功 toast 帶「復原」按鈕,右邊是按下復原後跳出的「已復原」。按「顯示 toast 數量」在兩種模式下都 alert 出 1。
這是 5 組各跑一次的結果,樣本很小,不能拿來算觸發率或錯誤率。它能說明的是:skill 裡的錯誤 API 確實會被照抄,而且照抄的那次沒有任何自我檢查。
⚙️ 怎麼安裝、怎麼點名?
只裝這一支、裝給 Claude Code,我實際跑過的指令(2026-10-21,skills@latest 解析到 1.7.4):
npx -y skills@latest add emilkowalski/skills --skill ask-sonner --agent claude-code --copy -y
不加 --agent 和 -y 會進互動選單:
npx skills@latest add emilkowalski/skills --skill ask-sonner
裝完專案裡會多出 .claude/skills/ask-sonner/SKILL.md、API.md 和一份 skills-lock.json。我把裝出來的兩個檔跟 repo 的 commit 2d22863 比對,內容一樣。安裝時 CLI 顯示的第三方評估是 Gen Safe、Socket 0 alerts、Snyk Low Risk。
點名的方式是把需求接在指令後面:
/ask-sonner 幫我把上傳流程改成 toast.promise,成功時帶一顆復原按鈕
🚧 適合誰?限制與坑
適合:專案已經用 Sonner、常遇到樣式蓋不掉或 toast 不出現的人。疑難排解表跟樣式升級梯子是作者本人的判斷,比 AI 自己猜可靠。剛要選 toast 函式庫的人,它也能幫你一次把 Toaster 放對位置。
不適合:沒用 Sonner 的專案。它不是通用的 toast 設計指南。
坑:
getActiveToasts不能用。要在 React 外面讀目前的 toast,用toast.getToasts()。這個錯在官方文件也有,所以不能靠「去看文件」來發現,要看型別檔。offset預設是 24px,不是 skill 和官方文件寫的 32px。要精確對齊設計稿時自己指定。- 沒點名不一定會觸發。我沒點名跑了 2 次,1 次有用上。
- 用上了不代表 AI 會驗證。這次照抄錯 API 的那組,連建置都沒跑。我的做法是在需求最後加一句「寫完跑
npx tsc -b確認能編譯」,或在 Claude Code 設一個寫檔後跑型別檢查的 hook。 - API.md 不是完整的 props 表。有些 2.0.8 支援的選項它沒列,AI 可能因此繞遠路。需要完整清單時直接看
node_modules/sonner/dist/index.d.ts。
🔗 延伸資源
- skill 原始碼:emilkowalski/skills — ask-sonner
- Sonner:GitHub、官方文件
- 同 repo 的 pick-ui-library:選元件函式庫時,它在 toast 這一格推薦的就是 Sonner。
- 同 repo 的 emil-design-eng:裡面有一節是作者做 Sonner 時歸納的元件設計原則。
- 同作者另一支 skill 的實測:apple-design skill 實測:17 節 Apple 動效規範,點名最保險
/ask-sonner 問一個你真的卡住的樣式問題,看它給的 classNames 或 headless 建議合不合用;要它寫程式時,記得叫它寫完跑一次 tsc。