回饋模式:Modal、Toast 與 Skeleton 全攻略 | TailwindCSS 完整教學
回饋(Feedback) 是介面對使用者每一次操作的回應——你點了儲存,它得告訴你成功了;你按了刪除,它得先問你確定嗎;資料還在載入,它得讓你知道別急。這一篇用 TailwindCSS 把原生
<dialog>Modal、右上角滑入的 Toast、純 CSS 的 Tooltip、四色語意的 Alert、animate-pulse的 skeleton 與 spinner,以及 Progress 進度條一次做齊,搭配 v4 的@starting-style進場動畫與少量 JS,兼顧美觀、流暢與無障礙。
前言
回饋(Feedback) 是一個介面的禮貌。表單是「使用者對網站說話」、導覽是「網站告訴使用者可以去哪」,那回饋就是「網站回應使用者剛剛做了什麼」——它讓每一次點擊、每一段等待、每一個危險操作都有明確的回音。少了回饋,使用者會困惑:我按到了嗎?存好了嗎?系統當掉了還是只是慢?好的回饋,在對的時機、用對的形式,把系統狀態清楚地傳達出去,而且不打擾。
先用一個生活化的類比。回饋 UI 就像一間餐廳的服務生:你點餐後他複誦一次「一份牛排,五分熟,對嗎?」——這是需要你確認的 Modal;餐點上桌時他輕聲說「這是您的牛排,請慢用」隨即離開,不打斷你談話——這是短暫出現又自動消失的 Toast;你盯著菜單上某道菜猶豫,他湊過來小聲補一句「這道會辣喔」——這是懸停才出現的 Tooltip;廚房正在備餐,桌上那張「餐點準備中」的小立牌讓你安心等待——這是 Loading skeleton。同樣是「傳達訊息」,打斷你的、路過提醒的、貼身補充的、安撫等待的,各有各的分寸。這正是回饋模式的核心:依打擾程度,選對回饋形式。
本系列以 TailwindCSS v4 為預設版本。本篇你將學到:
- 打斷式回饋:原生
<dialog>Modal,用showModal()免費取得 focus trap、::backdrop遮罩與 Escape 關閉 - 路過式回饋:Toast 通知的固定容器、堆疊排列與滑入/滑出過場,以及 Tooltip 的純 CSS 做法
- 狀態式回饋:四色語意 Alert/Banner、
animate-pulse的 skeleton 佔位、spinner 與 Progress 進度條 - v4 進場動畫:用
@starting-style/starting:變體做出純 CSS 的淡入放大,取代過去的 JS hack
本系列以 TailwindCSS v4 為預設版本,範例皆以 v4 語法示範。凡涉及 v3 差異之處,我會特別標註。
核心概念
回饋 UI 分類:依打擾程度各就各位
回饋元件看似零散,但可以依「對使用者的打擾程度」歸成四類,理解分類能幫你在對的場景選對工具:
| 類別 | 代表元件 | 打擾程度 | 典型用途 | 主要技術 |
|---|---|---|---|---|
| 打斷式 | Modal / Dialog | 高(需回應才能繼續) | 確認危險操作、表單彈窗 | <dialog>、showModal()、::backdrop |
| 路過式 | Toast / Snackbar | 中(短暫出現、自動消失) | 操作成功/失敗通知 | fixed 容器、@starting-style 滑入 |
| 貼身式 | Tooltip | 低(懸停才出現) | 圖示補充說明、輔助提示 | group + 絕對定位、純 CSS |
| 狀態式 | Alert / Skeleton / Progress | 低~中(常駐或載入時) | 重要提示、載入佔位、進度 | 語意色、animate-pulse、transition |
這張表值得記的一條線是:打擾程度越高,越該讓使用者能明確地回應或關閉。Modal 打斷整個流程,所以它必須能被 Escape、能被明確地確認或取消;Toast 只是路過,所以它必須自動消失、不能擋住操作;Tooltip 最輕,連點擊都不需要,懸停即現、移開即隱。把「打擾程度」和「可控制程度」對齊,是回饋設計的第一原則。
反過來說,選錯形式的代價很高。把一個不重要的「已複製到剪貼簿」做成需要點確認的 Modal,使用者每次都被強迫按一下,煩到想砸鍵盤;反之把「確定要永久刪除帳號嗎」做成一閃即逝的 Toast,使用者根本來不及反應就誤刪了。同樣地,把大量關鍵資訊塞進只有懸停才看得到的 Tooltip,等於把它藏起來——手機根本沒有懸停,那些內容就永遠見不到天日。所以在動手刻樣式之前,先問自己一個問題:這則訊息,使用者「必須」處理,還是「順便」知道就好? 必須處理的用 Modal 或常駐 Alert,順便知道的用 Toast 或 Tooltip。想清楚這一層,後面的 class 才有意義。
顏色語意與堆疊層級:兩個共通規範
不管做哪種回饋元件,有兩個跨元件的共通規範要先立好。
第一是顏色語意。 回饋 UI 高度依賴顏色傳達「這是好消息還是壞消息」,Tailwind 的色階剛好對應這套約定俗成的語意:
| 語意 | 顏色 | 用途 |
|---|---|---|
| Info | blue-* | 提示、說明性資訊 |
| Success | green-* | 操作成功、確認完成 |
| Warning | amber-* | 警告、需要注意 |
| Error | red-* | 錯誤、失敗、危險操作 |
| Neutral | gray-* | 系統訊息、一般狀態 |
但切記顏色不能是唯一的語意載體——色盲使用者可能分不出紅綠。每個回饋元件除了上色,都應該搭配一個對應的圖示(打勾、驚嘆號、叉叉),讓語意有雙重編碼。
第二是 z-index 堆疊層級。 回饋元件常常疊在一起(Modal 開著時跳出 Toast),誰蓋誰要事先規劃好:
| 層級 | z-index | 元件 |
|---|---|---|
| 一般內容 | z-0 ~ z-10 | 卡片、按鈕 |
| 固定導覽 | z-40 | sticky header、遮罩層 |
| Modal | z-50 | 對話框主體 |
| Toast | z-[70] | 通知(要在 Modal 之上,才不會被 Modal 蓋掉) |
| Tooltip | z-[80] | 提示(最上層,要在 Toast 之上) |
關鍵術語:role="status" 標記「非緊急」的動態內容(如 Toast 成功訊息),role="alert" 標記「緊急」的內容(如錯誤),兩者會通知螢幕閱讀器;aria-live="polite" 表示等當前朗讀完再通知、aria-live="assertive" 表示立即打斷(謹慎用);aria-modal="true" 標記 Modal 開啟時背景不可互動——用原生 <dialog> 的 showModal() 時,這些行為多半會自動處理好。
實作範例
理論看完,動手做三個最有代表性的回饋模式:一個原生 <dialog> Modal、一組Toast 通知(含滑入/滑出),以及skeleton 載入佔位。前提是你已在 globals.css 引入 Tailwind v4(@import "tailwindcss";)。
範例一:原生 <dialog> Modal 對話框
過去做 Modal 要自己刻一堆 div:遮罩層、focus trap、Escape 監聽、鎖背景捲動……現在原生 <dialog> 幾乎全包了。用 JS 呼叫 dialog.showModal() 開啟,瀏覽器就自動給你 focus trap、::backdrop 遮罩、Escape 關閉與背景 inert。我們只需用 Tailwind 上樣式,再用 v4 的 @starting-style 補一個進場動畫:
<button type="button" onclick="document.getElementById('confirm-dialog').showModal()"
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700">
刪除項目
</button>
<!-- 原生 dialog:showModal() 開啟時自動有 focus trap 與 ::backdrop -->
<dialog id="confirm-dialog"
class="m-auto w-full max-w-md rounded-2xl p-0 shadow-xl backdrop:bg-black/50 backdrop:backdrop-blur-sm
transition-all duration-200
starting:open:opacity-0 starting:open:scale-95
open:opacity-100 open:scale-100">
<div class="p-6">
<div class="flex items-start gap-4">
<!-- 圖示:語意不只靠顏色 -->
<div class="flex size-10 shrink-0 items-center justify-center rounded-full bg-red-100">
<svg class="size-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-900">確認刪除</h2>
<p class="mt-1 text-sm text-gray-500">確定要刪除此項目嗎?此操作無法復原。</p>
</div>
</div>
<!-- 用 form method="dialog":按鈕自動關閉 dialog,免寫 JS -->
<form method="dialog" class="mt-6 flex justify-end gap-3">
<button value="cancel"
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">
取消
</button>
<button value="confirm"
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700">
確認刪除
</button>
</form>
</div>
</dialog>
這段有三個關鍵值得拆解。showModal() 而非 show():只有 showModal() 才會啟用模態行為——focus trap、::backdrop 遮罩、Escape 關閉、背景 inert;用 show() 會變成非模態,失去這些保護,別搞錯。backdrop: 變體給遮罩上色:v4 的 backdrop: 對應 ::backdrop 偽元素,backdrop:bg-black/50 backdrop:backdrop-blur-sm 讓遮罩半透明加毛玻璃,完全不用自己刻遮罩 div。starting:open: 做進場動畫:open: 對應 <dialog> 開啟後自動加上的 open 屬性,starting:open:opacity-0 starting:open:scale-95 宣告「剛開啟那一刻是透明且縮小的」,配合 transition-all 就得到純 CSS 的淡入放大進場——這正是 @starting-style 解決的老問題。最後,<form method="dialog"> 是個甜點:表單內按鈕按下會自動關閉 dialog 並回傳 value,連關閉的 JS 都省了。
範例二:Toast 通知(滑入 + 自動消失 + 滑出)
Toast 的骨架是一個固定在角落的容器,新通知用 JS 插入其中,靠 flex flex-col gap-3 自動堆疊。進場用 @starting-style 從右滑入,幾秒後自動離場——離場要在移除前先加離場 class、等過渡結束再真正 remove,才不會瞬間消失把下面的 Toast 猛然頂上去:
<!-- Toast 容器:固定右上、縱向堆疊。z-[70] 要在 Modal 之上 -->
<!-- 容器 pointer-events-none 讓透明區不擋點擊,卡片再各自 auto -->
<div id="toast-container"
class="pointer-events-none fixed top-4 right-4 z-[70] flex flex-col gap-3"
aria-live="polite" aria-atomic="false"></div>
<button type="button" onclick="showToast('success', '文章已成功儲存。')"
class="rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium text-white hover:bg-cyan-700">
觸發成功 Toast
</button>
<script>
const ICONS = {
success: { border: 'border-green-200', bg: 'bg-green-100', fg: 'text-green-600', role: 'status',
path: 'M5 13l4 4L19 7', title: '操作成功' },
error: { border: 'border-red-200', bg: 'bg-red-100', fg: 'text-red-600', role: 'alert',
path: 'M6 18L18 6M6 6l12 12', title: '操作失敗' },
};
function showToast(type, message, timeout = 4000) {
const c = ICONS[type];
const toast = document.createElement('div');
toast.setAttribute('role', c.role);
// 初始就是「進場前」的樣式,插入 DOM 後下一幀移除 starting class 觸發過渡
toast.className =
`pointer-events-auto flex w-80 items-start gap-3 rounded-xl border ${c.border} bg-white p-4 shadow-lg
transition-all duration-300 translate-x-full opacity-0`;
toast.innerHTML = `
<div class="flex size-8 shrink-0 items-center justify-center rounded-full ${c.bg}">
<svg class="size-4 ${c.fg}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${c.path}" />
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-gray-900">${c.title}</p>
<p class="mt-0.5 text-xs text-gray-500">${message}</p>
</div>`;
document.getElementById('toast-container').appendChild(toast);
// 進場:下一幀切到最終狀態,transition 生效 → 從右滑入
requestAnimationFrame(() => {
toast.classList.remove('translate-x-full', 'opacity-0');
});
// 離場:先加離場 class,等過渡結束再真正移除,避免版面瞬跳
setTimeout(() => {
toast.classList.add('translate-x-full', 'opacity-0');
toast.addEventListener('transitionend', () => toast.remove(), { once: true });
}, timeout);
}
</script>
拆解 Toast 的三個要點。容器負責堆疊、卡片負責過場:容器用 fixed top-4 right-4 flex flex-col gap-3,不管有幾個 Toast 都靠 flex + gap 自動排列,絕不讓每個 Toast 各自 fixed 而互相重疊。進出場都要有過渡:進場靠 requestAnimationFrame 在下一幀移除 translate-x-full opacity-0(這是純 JS 版的 @starting-style 心法);離場則先加回這兩個 class、監聽 transitionend 再 remove(),滑出後其他 Toast 才平滑遞補上來。pointer-events 兩段式:容器 pointer-events-none 讓它不擋住底下內容的點擊,卡片再各自 pointer-events-auto 保留自己的互動——這是 Toast 疊在頁面上卻不干擾操作的關鍵。至於 aria-live="polite",讓螢幕閱讀器在朗讀空檔播報新 Toast,不會粗暴打斷。
範例三:Skeleton 載入佔位
資料還沒回來時,與其顯示一個孤零零的 spinner,不如用 skeleton——長得像最終內容的灰色佔位,animate-pulse 做出呼吸般的閃爍。它最大的價值是維持版面結構:等真實內容載入後直接替換,佈局不會突然撐開跳動:
<!-- 卡片 Skeleton:形狀對齊真實卡片,animate-pulse 讓整體閃爍 -->
<div class="animate-pulse overflow-hidden rounded-2xl border border-gray-200 bg-white">
<!-- 圖片佔位:aspect-video 佔住和真圖一樣的比例,這是防跳動的關鍵 -->
<div class="aspect-video bg-gray-200"></div>
<div class="space-y-3 p-5">
<!-- 標籤佔位 -->
<div class="flex gap-2">
<div class="h-5 w-16 rounded-full bg-gray-200"></div>
<div class="h-5 w-20 rounded-full bg-gray-200"></div>
</div>
<!-- 標題佔位(兩行,第二行較短模擬真實文字) -->
<div class="space-y-2">
<div class="h-4 w-full rounded-lg bg-gray-200"></div>
<div class="h-4 w-3/4 rounded-lg bg-gray-200"></div>
</div>
<!-- 作者佔位 -->
<div class="flex items-center gap-3 border-t border-gray-100 pt-3">
<div class="size-7 rounded-full bg-gray-200"></div>
<div class="space-y-1.5">
<div class="h-3 w-20 rounded bg-gray-200"></div>
<div class="h-2.5 w-14 rounded bg-gray-200"></div>
</div>
</div>
</div>
</div>
skeleton 的精髓在形狀要對齊真實內容:圖片佔位用 aspect-video(和真圖同比例)、標題佔位用 h-4 兩行(第二行 w-3/4 較短,模擬真實段落的參差)、頭像佔位用 size-7 rounded-full。整個外層加 animate-pulse,Tailwind 內建的這個動畫會讓透明度來回呼吸,暗示「內容正在來的路上」。佔位尺寸越貼近真實內容,載入完成時的替換就越無感——這是 skeleton 勝過 spinner 的核心理由:spinner 只說「在轉」,skeleton 直接把最終版面先撐住。
實務上還有一個心法值得記:skeleton 應該是真實元件的「灰階雙胞胎」。最理想的做法,是讓 skeleton 和真實卡片共用同一套外層結構(同樣的 rounded-2xl、p-5、space-y-3),只是把裡面的文字、圖片換成灰色方塊。這樣不只尺寸對得上,連間距、圓角、邊框都一致,載入完成的那一刻就像有人把灰色濾鏡緩緩拿掉,毫無突兀。反之,如果 skeleton 是另外隨手畫的一組灰線,和真實卡片的排版對不起來,反而製造了「載入前後長得不一樣」的閃爍感,比不做還糟。因此在元件化的專案裡,常見的做法是把 skeleton 和真實元件寫在一起、共用容器,只依 loading 狀態切換內部要顯示灰塊還是真資料。
若只是按鈕內的短暫等待,或不需要維持結構的場景,一個 spinner 就夠了:
<!-- 按鈕內嵌 spinner:animate-spin + 環形 SVG -->
<button disabled
class="inline-flex items-center gap-2 rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium text-white opacity-75">
<svg class="size-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
處理中...
</button>
順帶做一個 Progress 進度條,用於「已知進度」的載入(如檔案上傳)。外層是灰色軌道、內層用 width 表示進度,transition-all 讓進度變化平滑,並用 role="progressbar" 與 aria-valuenow 讓螢幕閱讀器讀得到:
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="font-medium text-gray-700">上傳進度</span>
<span class="text-gray-500">68%</span>
</div>
<div class="h-2 overflow-hidden rounded-full bg-gray-200">
<div class="h-full rounded-full bg-cyan-600 transition-all duration-500 ease-out"
style="width: 68%"
role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100"
aria-label="上傳進度 68%"></div>
</div>
</div>
補充:Tooltip 與 Alert 橫幅
最後補兩個常用的輕量回饋。Tooltip 是最不打擾的提示,用 group + 絕對定位,純 CSS 就能做——懸停父層時子層的提示框現身;別忘了 group-focus-within: 讓鍵盤 Tab 進去也能觸發:
<div class="group relative inline-flex">
<button class="rounded-lg p-2 text-gray-500 hover:bg-gray-100" aria-describedby="tip">
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<!-- 懸停或聚焦父層時現身。z-[80] 在最上層 -->
<div id="tip" role="tooltip"
class="invisible absolute bottom-full left-1/2 z-[80] mb-2 -translate-x-1/2 whitespace-nowrap
rounded-lg bg-gray-900 px-3 py-1.5 text-xs text-white opacity-0 transition-all duration-150
group-hover:visible group-hover:opacity-100
group-focus-within:visible group-focus-within:opacity-100">
此操作無法復原
</div>
</div>
Alert 橫幅則是常駐頁面的重要提示,和 Toast 不同——它不會自動消失,適合放持續性的訊息(認證提醒、配額警告)。四種語意色各配一個圖示,role 依緊急程度選 status 或 alert:
<!-- Warning 橫幅:amber 語意 + 圖示,role="alert" 通知螢幕閱讀器 -->
<div role="alert"
class="flex items-start gap-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-amber-800">
<svg class="mt-0.5 size-4 shrink-0 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div class="text-sm">
<p class="font-medium">儲存空間警告</p>
<p class="mt-0.5 text-amber-700">您已使用 85% 的儲存空間,請考慮升級方案。</p>
</div>
</div>
常見錯誤與最佳實踐
坑一:自刻 Modal 卻漏掉 focus trap 與 Escape
最常見的無障礙災難,是用一堆 div 加 z-index 硬堆一個 Modal,結果背後全是漏洞:Tab 鍵能跑到 Modal 後面的頁面元素、按 Escape 沒反應、關閉後焦點不知飛去哪、背景還能捲動:
<!-- ❌ 純 div Modal:沒有 focus trap、沒有 Escape、背景可互動 -->
<div class="fixed inset-0 z-50 bg-black/50">
<div class="m-auto max-w-md rounded-2xl bg-white p-6">...</div>
</div>
正確做法:優先用原生 <dialog> + showModal()(範例一),它一次補齊 focus trap、::backdrop、Escape 與背景 inert。若因需求(側拉 Drawer、多層堆疊)非自刻不可,那 focus trap、Escape 監聽、aria-modal="true"、關閉後焦點回歸觸發元素、鎖背景捲動這五件事,一件都不能省。
坑二:Toast 堆疊亂跳、透明區擋住點擊
第一種亂跳來自每個 Toast 各自 fixed 定位而互相重疊;第二種來自新增/移除時沒有過場,元素瞬間出現消失把兄弟猛然頂動;還有一種是 Toast 容器蓋滿整個角落,透明區域擋住了底下按鈕的點擊:
<!-- ❌ 每個 Toast 各自 fixed → 全部疊在同一位置 -->
<div class="fixed top-4 right-4 ...">Toast A</div>
<div class="fixed top-4 right-4 ...">Toast B</div>
正確做法:一個 fixed 容器加 flex flex-col gap-3,Toast 當它的子元素靠 flex 自動堆疊(範例二);進出場都用 transition + 位移過渡,離場先加離場 class、等 transitionend 再 remove;容器 pointer-events-none、卡片 pointer-events-auto,透明區就不擋點擊了。堆疊也要設上限,同時最多 3-5 個。
坑三:skeleton 尺寸和真實內容對不上,載入完照樣跳動
skeleton 的全部意義就是防止版面跳動,但如果佔位的尺寸和真實內容差很多——圖片佔位沒設高度、文字佔位只有一行但真內容有五行——載入完成的瞬間版面照樣暴衝,skeleton 白做了:
<!-- ❌ 圖片佔位沒有高度、文字佔位太少 → 真內容一來版面撐開 -->
<div class="animate-pulse">
<div class="bg-gray-200"></div> <!-- 沒高度,等於沒佔位 -->
<div class="h-4 w-full bg-gray-200"></div> <!-- 只一行,真內容三行 -->
</div>
正確做法:佔位形狀盡量貼合真實內容——圖片用 aspect-video 或明確高度佔住比例、文字用和真內容相近的行數(範例三)。記住 skeleton 是「先把最終版面撐住」的預留位,不是隨便畫幾條灰線。
坑四:回饋只靠顏色、動畫不尊重使用者偏好
兩個容易忽略的細節。其一,Alert/Toast 只靠紅綠上色卻沒圖示,色盲使用者分不出成功或失敗;其二,一堆 animate-pulse、animate-spin 動個不停,對前庭功能敏感的使用者是負擔:
<!-- ❌ 只靠顏色分語意、動畫不可關 -->
<div class="bg-green-50 text-green-800">操作成功</div>
正確做法:每個語意回饋都配一個對應圖示(打勾/叉叉/驚嘆號),讓語意有顏色 + 圖示雙重編碼;動畫則用 motion-reduce: 變體尊重系統設定,例如 animate-pulse motion-reduce:animate-none,當使用者開啟「減少動態效果」時自動停用。
最佳實踐小結
- Modal 優先原生
<dialog>:showModal()免費送 focus trap、::backdrop、Escape 與背景inert,別再堆div。 - 進場動畫用
@starting-style:v4 的starting:變體做純 CSS 淡入放大,取代舊的requestAnimationFramehack。 - Toast 容器堆疊、卡片過場:
fixed容器 +flex flex-col gap-3,進出場都有過渡,pointer-events兩段式。 - skeleton 尺寸貼合真實內容:圖片用
aspect-video佔比例,防止載入完成時版面跳動。 - 無障礙與偏好:
role="status"/alert通知、顏色配圖示雙重編碼、motion-reduce:animate-none尊重動態偏好。
小結
這是 TailwindCSS 完整教學 系列的第三十六篇。上一篇《導覽模式》,我們把整站的移動動線——navbar、下拉選單、Drawer——一路做齊;這一篇,我們把視線從「使用者往哪裡去」轉到「網站如何回應使用者剛剛做了什麼」,看回饋 UI 如何在每一次點擊、每一段等待後給出清楚又不打擾的回音。回顧幾個重點:
- 四種回饋形態:打斷式(Modal)、路過式(Toast)、貼身式(Tooltip)、狀態式(Alert/Skeleton/Progress),依打擾程度選對形式。
- 原生
<dialog>Modal:showModal()自動處理 focus trap、::backdrop遮罩、Escape 與背景inert,Tailwind 只負責上樣式。 - v4 進場動畫:
@starting-style/starting:變體解決「元素從無到有」的過渡難題,做出純 CSS 的淡入放大。 - Toast 與 skeleton:Toast 靠容器堆疊 + 卡片進出場過渡、
pointer-events兩段式;skeleton 靠尺寸貼合真實內容防版面跳動。 - 無障礙:
role="status"/alert與aria-live通知螢幕閱讀器、顏色配圖示雙重編碼、motion-reduce:尊重動態偏好。
至此,我們完成了 TW-6 元件模式 的完整旅程——從基礎元件、表單、導覽到回饋,你已經能用 TailwindCSS 拼出一個完整介面的每一塊。接下來系列進入 TW-7 生態系與整合,下一篇《建置工具整合》,將帶你把目光從「怎麼寫 class」拉到「怎麼把 Tailwind 接進真實專案」——看它如何與 Vite、PostCSS 等建置工具協作,讓開發流程順暢又高效。