回饋模式:Modal、Toast 與 Skeleton 全攻略 | TailwindCSS 完整教學

2026/09/05
回饋模式:Modal、Toast 與 Skeleton 全攻略 | TailwindCSS 完整教學

回饋(Feedback) 是介面對使用者每一次操作的回應——你點了儲存,它得告訴你成功了;你按了刪除,它得先問你確定嗎;資料還在載入,它得讓你知道別急。這一篇用 TailwindCSS 把原生 <dialog> Modal、右上角滑入的 Toast、純 CSS 的 Tooltip、四色語意的 Alertanimate-pulseskeleton 與 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/Banneranimate-pulseskeleton 佔位、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-pulsetransition

這張表值得記的一條線是:打擾程度越高,越該讓使用者能明確地回應或關閉。Modal 打斷整個流程,所以它必須能被 Escape、能被明確地確認或取消;Toast 只是路過,所以它必須自動消失、不能擋住操作;Tooltip 最輕,連點擊都不需要,懸停即現、移開即隱。把「打擾程度」和「可控制程度」對齊,是回饋設計的第一原則。

反過來說,選錯形式的代價很高。把一個不重要的「已複製到剪貼簿」做成需要點確認的 Modal,使用者每次都被強迫按一下,煩到想砸鍵盤;反之把「確定要永久刪除帳號嗎」做成一閃即逝的 Toast,使用者根本來不及反應就誤刪了。同樣地,把大量關鍵資訊塞進只有懸停才看得到的 Tooltip,等於把它藏起來——手機根本沒有懸停,那些內容就永遠見不到天日。所以在動手刻樣式之前,先問自己一個問題:這則訊息,使用者「必須」處理,還是「順便」知道就好? 必須處理的用 Modal 或常駐 Alert,順便知道的用 Toast 或 Tooltip。想清楚這一層,後面的 class 才有意義。

顏色語意與堆疊層級:兩個共通規範

不管做哪種回饋元件,有兩個跨元件的共通規範要先立好。

第一是顏色語意。 回饋 UI 高度依賴顏色傳達「這是好消息還是壞消息」,Tailwind 的色階剛好對應這套約定俗成的語意:

語意顏色用途
Infoblue-*提示、說明性資訊
Successgreen-*操作成功、確認完成
Warningamber-*警告、需要注意
Errorred-*錯誤、失敗、危險操作
Neutralgray-*系統訊息、一般狀態

但切記顏色不能是唯一的語意載體——色盲使用者可能分不出紅綠。每個回饋元件除了上色,都應該搭配一個對應的圖示(打勾、驚嘆號、叉叉),讓語意有雙重編碼。

第二是 z-index 堆疊層級。 回饋元件常常疊在一起(Modal 開著時跳出 Toast),誰蓋誰要事先規劃好:

層級z-index元件
一般內容z-0 ~ z-10卡片、按鈕
固定導覽z-40sticky header、遮罩層
Modalz-50對話框主體
Toastz-[70]通知(要在 Modal 之上,才不會被 Modal 蓋掉)
Tooltipz-[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 讓遮罩半透明加毛玻璃,完全不用自己刻遮罩 divstarting: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、監聽 transitionendremove(),滑出後其他 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-2xlp-5space-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 依緊急程度選 statusalert:

<!-- 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

最常見的無障礙災難,是用一堆 divz-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、等 transitionendremove;容器 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-pulseanimate-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 淡入放大,取代舊的 requestAnimationFrame hack。
  • 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"/alertaria-live 通知螢幕閱讀器、顏色配圖示雙重編碼、motion-reduce: 尊重動態偏好。

至此,我們完成了 TW-6 元件模式 的完整旅程——從基礎元件、表單、導覽到回饋,你已經能用 TailwindCSS 拼出一個完整介面的每一塊。接下來系列進入 TW-7 生態系與整合,下一篇《建置工具整合》,將帶你把目光從「怎麼寫 class」拉到「怎麼把 Tailwind 接進真實專案」——看它如何與 Vite、PostCSS 等建置工具協作,讓開發流程順暢又高效。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →