<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>模組六：互動回饋設計 on Tarragon</title><link>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/</link><description>Recent content in 模組六：互動回饋設計 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 13 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/index.xml" rel="self" type="application/rss+xml"/><item><title>互動回饋三層模型：點擊確認、等待指示、結果通知</title><link>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/feedback-three-layers/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/feedback-three-layers/</guid><description>&lt;h2 id="核心觀念">核心觀念&lt;/h2>
&lt;p>介面設計中，&lt;strong>使用者每執行一個動作，系統都必須提供回饋&lt;/strong>。回饋是可用性的基本要求。沒有回饋的按鈕，使用者無法區分「系統在處理」和「按鈕壞了」。&lt;/p>
&lt;p>這個原則對應 Jakob Nielsen 十大可用性啟發法的第一條：&lt;strong>系統狀態可見性（Visibility of System Status）&lt;/strong> — 系統應透過適當的回饋，在合理時間內告知使用者目前發生了什麼。&lt;/p>
&lt;h2 id="三層回饋模型">三層回饋模型&lt;/h2>
&lt;p>使用者按下按鈕後，回饋依時間順序分三層：&lt;/p>
&lt;h3 id="第一層點擊確認0-100ms">第一層：點擊確認（0-100ms）&lt;/h3>
&lt;p>系統確認「收到你的操作了」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>回饋形式&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;th>適用場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>視覺狀態變化&lt;/td>
 &lt;td>按鈕顏色變深、凹陷效果&lt;/td>
 &lt;td>所有按鈕&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>水波紋效果&lt;/td>
 &lt;td>Material Design ripple&lt;/td>
 &lt;td>觸控介面&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>觸覺回饋&lt;/td>
 &lt;td>震動&lt;/td>
 &lt;td>行動裝置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>聲音回饋&lt;/td>
 &lt;td>輕微點擊聲&lt;/td>
 &lt;td>無障礙輔助&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>設計原則&lt;/strong>：第一層回饋必須在 100ms 內出現。這個門檻來自人類感知研究 — 100ms 以內的延遲被感知為即時反應，超過後點擊與回饋之間開始出現可察覺的落差，回饋就失去「確認」的效果。平台原生元件的 pressed / ripple 互動態已滿足這個門檻，用平台預設互動態即自動合格；自建元件（自繪按鈕、canvas UI）才需要自行驗證回饋延遲。&lt;/p>
&lt;h3 id="第二層等待指示100ms---10s">第二層：等待指示（100ms - 10s）&lt;/h3>
&lt;p>系統告知「正在處理你的請求」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>等待時長&lt;/th>
 &lt;th>建議回饋&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&amp;lt; 400ms&lt;/td>
 &lt;td>不需額外指示&lt;/td>
 &lt;td>Doherty Threshold：400ms 內完成、人機互動不需互相等待&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>400ms - 1s&lt;/td>
 &lt;td>Spinner / 按鈕 loading 狀態&lt;/td>
 &lt;td>簡單指示「還在跑」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1s - 10s&lt;/td>
 &lt;td>進度條或步驟指示&lt;/td>
 &lt;td>告知使用者「到哪了」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;gt; 10s&lt;/td>
 &lt;td>進度百分比 + 預估剩餘時間&lt;/td>
 &lt;td>超過 10s 使用者開始考慮放棄&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>設計原則&lt;/strong>：等待期間必須禁用觸發按鈕（disabled 狀態），防止使用者因焦慮而重複點擊導致重複提交。UI 層的防重複提交降低發生率；最終防線是伺服器端的冪等設計 — 重複請求真的到達後端時要能被識別與去重。兩層都要做，本系列只涵蓋 UI 層。&lt;/p>
&lt;p>&lt;strong>400ms 門檻（&lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/doherty-threshold/" data-link-title="Doherty Threshold（400ms 門檻）" data-link-desc="說明 400ms 回應時間門檻的出身（IBM 生產力研究）、現代設計慣例的轉譯過程，以及它跟 Nielsen 感知門檻的量測差異">Doherty Threshold&lt;/a>）&lt;/strong> 的實務意義：非同步操作通常在 400ms 內完成時，可省略 loading 指示器、直接跳到第三層的結果回饋（點擊確認仍要在第一層提供）。這條門檻的出身與適用邊界、以及實際延遲有快有慢時怎麼對表，&lt;a href="../response-time-strategy/">時間感知與回應策略&lt;/a>有完整推導。&lt;/p>
&lt;h3 id="第三層結果通知">第三層：結果通知&lt;/h3>
&lt;p>系統告知「操作的結果是什麼」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>結果&lt;/th>
 &lt;th>回饋形式&lt;/th>
 &lt;th>設計要求&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>成功&lt;/td>
 &lt;td>畫面更新 / 成功提示 / 跳轉&lt;/td>
 &lt;td>讓使用者看到操作造成的改變&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗&lt;/td>
 &lt;td>錯誤訊息 + 可執行動作&lt;/td>
 &lt;td>告知原因 + 提供下一步（重試 / 替代方案）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>空結果&lt;/td>
 &lt;td>空狀態畫面&lt;/td>
 &lt;td>區別「沒找到」和「沒載入」&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>部分成功&lt;/td>
 &lt;td>摘要 + 明細&lt;/td>
 &lt;td>告知哪些成功哪些失敗&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>設計原則&lt;/strong>：結果通知必須恢復按鈕到可操作狀態。操作結束後按鈕仍顯示 loading = 介面凍結。&lt;/p>
&lt;p>&lt;strong>空結果的呈現&lt;/strong>：空狀態畫面要讓使用者分得出「查詢成功但沒有資料」和「還沒載入完」— 給明確文字（「沒有符合的結果」）與下一步建議（放寬條件、建立第一筆資料），載入中則維持等待指示。空狀態與錯誤狀態的措辭由&lt;a href="https://tarrragon.github.io/blog/ux-design/04-error-recovery/error-message-principles/" data-link-title="錯誤訊息撰寫原則" data-link-desc="錯誤訊息的兩個職責：使用者能讀懂發生什麼、使用者能決定下一步做什麼">模組四：錯誤訊息撰寫原則&lt;/a>接手。&lt;/p>
&lt;p>&lt;strong>部分成功的呈現&lt;/strong>：批次操作（匯入 100 筆、87 筆成功）用摘要加明細兩層：摘要先告知整體結果（「87 筆成功、13 筆失敗」），明細列出失敗項與各自的修正動作，讓使用者只需處理失敗的部分、而非重跑整批。&lt;/p>
&lt;p>&lt;strong>結果通知的鏈路前提&lt;/strong>：第三層的設計都假設「結果會正確到達 UI」。UI 與執行端分屬不同 context 時（browser extension 的 popup / content script / service worker、跨 process 架構），結果要跨訊息通道才到 UI — 通道語意錯誤或事件訂閱缺失會讓通知消失、甚至反轉。一個 Chrome extension 提取成功 96 本且已寫入 storage，popup 卻顯示「提取失敗」— async listener 把 undefined 搶先當回應送回（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/async-listener-false-failure/" data-link-title="U.C9 提取成功卻誤報失敗 — 結果通知鏈路被搶通道" data-link-desc="操作實際成功、資料已寫入，UI 卻顯示失敗時使用。多 context 的訊息通道語意（誰負責回應）是結果通知鏈路的一部分，async listener 搶通道會把 undefined 當成回應送回">U.C9&lt;/a>）。跨 context 的結果通知要有端對端驗證（操作成功 → UI 顯示成功），不只測 UI 收到資料後的呈現。&lt;/p>
&lt;p>本篇聚焦「該通知什麼」；SnackBar / Dialog / Banner / Bottom Sheet 之間的形式選擇由&lt;a href="../notification-pattern-selection/">通知模式選擇&lt;/a>展開。螢幕閱讀器的通知宣告（aria-live / aria-busy）屬無障礙實作，不在本模組範圍。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心觀念">核心觀念</h2>
<p>介面設計中，<strong>使用者每執行一個動作，系統都必須提供回饋</strong>。回饋是可用性的基本要求。沒有回饋的按鈕，使用者無法區分「系統在處理」和「按鈕壞了」。</p>
<p>這個原則對應 Jakob Nielsen 十大可用性啟發法的第一條：<strong>系統狀態可見性（Visibility of System Status）</strong> — 系統應透過適當的回饋，在合理時間內告知使用者目前發生了什麼。</p>
<h2 id="三層回饋模型">三層回饋模型</h2>
<p>使用者按下按鈕後，回饋依時間順序分三層：</p>
<h3 id="第一層點擊確認0-100ms">第一層：點擊確認（0-100ms）</h3>
<p>系統確認「收到你的操作了」。</p>
<table>
  <thead>
      <tr>
          <th>回饋形式</th>
          <th>範例</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>視覺狀態變化</td>
          <td>按鈕顏色變深、凹陷效果</td>
          <td>所有按鈕</td>
      </tr>
      <tr>
          <td>水波紋效果</td>
          <td>Material Design ripple</td>
          <td>觸控介面</td>
      </tr>
      <tr>
          <td>觸覺回饋</td>
          <td>震動</td>
          <td>行動裝置</td>
      </tr>
      <tr>
          <td>聲音回饋</td>
          <td>輕微點擊聲</td>
          <td>無障礙輔助</td>
      </tr>
  </tbody>
</table>
<p><strong>設計原則</strong>：第一層回饋必須在 100ms 內出現。這個門檻來自人類感知研究 — 100ms 以內的延遲被感知為即時反應，超過後點擊與回饋之間開始出現可察覺的落差，回饋就失去「確認」的效果。平台原生元件的 pressed / ripple 互動態已滿足這個門檻，用平台預設互動態即自動合格；自建元件（自繪按鈕、canvas UI）才需要自行驗證回饋延遲。</p>
<h3 id="第二層等待指示100ms---10s">第二層：等待指示（100ms - 10s）</h3>
<p>系統告知「正在處理你的請求」。</p>
<table>
  <thead>
      <tr>
          <th>等待時長</th>
          <th>建議回饋</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&lt; 400ms</td>
          <td>不需額外指示</td>
          <td>Doherty Threshold：400ms 內完成、人機互動不需互相等待</td>
      </tr>
      <tr>
          <td>400ms - 1s</td>
          <td>Spinner / 按鈕 loading 狀態</td>
          <td>簡單指示「還在跑」</td>
      </tr>
      <tr>
          <td>1s - 10s</td>
          <td>進度條或步驟指示</td>
          <td>告知使用者「到哪了」</td>
      </tr>
      <tr>
          <td>&gt; 10s</td>
          <td>進度百分比 + 預估剩餘時間</td>
          <td>超過 10s 使用者開始考慮放棄</td>
      </tr>
  </tbody>
</table>
<p><strong>設計原則</strong>：等待期間必須禁用觸發按鈕（disabled 狀態），防止使用者因焦慮而重複點擊導致重複提交。UI 層的防重複提交降低發生率；最終防線是伺服器端的冪等設計 — 重複請求真的到達後端時要能被識別與去重。兩層都要做，本系列只涵蓋 UI 層。</p>
<p><strong>400ms 門檻（<a href="/blog/ux-design/knowledge-cards/doherty-threshold/" data-link-title="Doherty Threshold（400ms 門檻）" data-link-desc="說明 400ms 回應時間門檻的出身（IBM 生產力研究）、現代設計慣例的轉譯過程，以及它跟 Nielsen 感知門檻的量測差異">Doherty Threshold</a>）</strong> 的實務意義：非同步操作通常在 400ms 內完成時，可省略 loading 指示器、直接跳到第三層的結果回饋（點擊確認仍要在第一層提供）。這條門檻的出身與適用邊界、以及實際延遲有快有慢時怎麼對表，<a href="../response-time-strategy/">時間感知與回應策略</a>有完整推導。</p>
<h3 id="第三層結果通知">第三層：結果通知</h3>
<p>系統告知「操作的結果是什麼」。</p>
<table>
  <thead>
      <tr>
          <th>結果</th>
          <th>回饋形式</th>
          <th>設計要求</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>成功</td>
          <td>畫面更新 / 成功提示 / 跳轉</td>
          <td>讓使用者看到操作造成的改變</td>
      </tr>
      <tr>
          <td>失敗</td>
          <td>錯誤訊息 + 可執行動作</td>
          <td>告知原因 + 提供下一步（重試 / 替代方案）</td>
      </tr>
      <tr>
          <td>空結果</td>
          <td>空狀態畫面</td>
          <td>區別「沒找到」和「沒載入」</td>
      </tr>
      <tr>
          <td>部分成功</td>
          <td>摘要 + 明細</td>
          <td>告知哪些成功哪些失敗</td>
      </tr>
  </tbody>
</table>
<p><strong>設計原則</strong>：結果通知必須恢復按鈕到可操作狀態。操作結束後按鈕仍顯示 loading = 介面凍結。</p>
<p><strong>空結果的呈現</strong>：空狀態畫面要讓使用者分得出「查詢成功但沒有資料」和「還沒載入完」— 給明確文字（「沒有符合的結果」）與下一步建議（放寬條件、建立第一筆資料），載入中則維持等待指示。空狀態與錯誤狀態的措辭由<a href="/blog/ux-design/04-error-recovery/error-message-principles/" data-link-title="錯誤訊息撰寫原則" data-link-desc="錯誤訊息的兩個職責：使用者能讀懂發生什麼、使用者能決定下一步做什麼">模組四：錯誤訊息撰寫原則</a>接手。</p>
<p><strong>部分成功的呈現</strong>：批次操作（匯入 100 筆、87 筆成功）用摘要加明細兩層：摘要先告知整體結果（「87 筆成功、13 筆失敗」），明細列出失敗項與各自的修正動作，讓使用者只需處理失敗的部分、而非重跑整批。</p>
<p><strong>結果通知的鏈路前提</strong>：第三層的設計都假設「結果會正確到達 UI」。UI 與執行端分屬不同 context 時（browser extension 的 popup / content script / service worker、跨 process 架構），結果要跨訊息通道才到 UI — 通道語意錯誤或事件訂閱缺失會讓通知消失、甚至反轉。一個 Chrome extension 提取成功 96 本且已寫入 storage，popup 卻顯示「提取失敗」— async listener 把 undefined 搶先當回應送回（<a href="/blog/ux-design/cases/async-listener-false-failure/" data-link-title="U.C9 提取成功卻誤報失敗 — 結果通知鏈路被搶通道" data-link-desc="操作實際成功、資料已寫入，UI 卻顯示失敗時使用。多 context 的訊息通道語意（誰負責回應）是結果通知鏈路的一部分，async listener 搶通道會把 undefined 當成回應送回">U.C9</a>）。跨 context 的結果通知要有端對端驗證（操作成功 → UI 顯示成功），不只測 UI 收到資料後的呈現。</p>
<p>本篇聚焦「該通知什麼」；SnackBar / Dialog / Banner / Bottom Sheet 之間的形式選擇由<a href="../notification-pattern-selection/">通知模式選擇</a>展開。螢幕閱讀器的通知宣告（aria-live / aria-busy）屬無障礙實作，不在本模組範圍。</p>
<h2 id="兩類按鈕的回饋設計">兩類按鈕的回饋設計</h2>
<p>依操作的時間特性，按鈕分兩類（各狀態的視覺設計在<a href="../button-state-design/">按鈕狀態設計</a>展開）：</p>
<h3 id="非同步按鈕api--資料庫--權限請求">非同步按鈕（API / 資料庫 / 權限請求）</h3>
<p>完整三層回饋：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[idle] → 點擊 → [loading: disabled + spinner] → 完成 → [idle + 結果通知]</span></span></code></pre></div><p>設計要點：</p>
<ul>
<li>loading 期間按鈕 disabled（防重複提交）</li>
<li>按鈕文字可變（「送出」→「送出中&hellip;」）</li>
<li>結束後恢復 idle 狀態</li>
</ul>
<h3 id="同步按鈕導航--切換--開關">同步按鈕（導航 / 切換 / 開關）</h3>
<p>僅需第一層 + 防連點：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[idle] → 點擊 → [視覺回饋 + 執行] → [完成]</span></span></code></pre></div><p>設計要點：</p>
<ul>
<li>操作在 100ms 內完成，不需 loading</li>
<li>加入防連點（300ms 上下的慣例值）避免快速連點觸發多次導航</li>
<li>視覺回饋（ripple / 狀態切換）即足夠</li>
</ul>
<p><strong>防連點的語意</strong>：第一次點擊立即執行、之後 300ms 內的點擊忽略（leading-edge <a href="/blog/ux-design/knowledge-cards/debounce/" data-link-title="Debounce（防連點）" data-link-desc="說明合併短時間內重複觸發的技巧，以及 leading-edge 與 trailing-edge 兩種執行語意在按鈕防連點上的差別">debounce</a>），或用導航鎖（in-flight flag）在導航完成前拒絕新請求。反過來「等 300ms 沒有新點擊才執行」（trailing debounce）會給每次導航加上 300ms 延遲，直接違反第一層的 100ms 門檻 — 防連點防的是第二次點擊，第一次要立即生效。</p>
<h2 id="畫面級回饋多步驟流程的狀態轉換">畫面級回饋：多步驟流程的狀態轉換</h2>
<p>上述按鈕級回饋處理的是「按一個按鈕」的單一操作。但有些使用者流程涉及<strong>整個畫面的狀態轉換</strong> — 連線、配對、同步、多步驟表單 — 每個中間狀態都需要能被使用者區辨的 UI 回饋。</p>
<h3 id="與按鈕級的差異">與按鈕級的差異</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>按鈕級回饋</th>
          <th>畫面級回饋</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>範圍</td>
          <td>單一按鈕內的狀態變化</td>
          <td>整個畫面 UI 替換</td>
      </tr>
      <tr>
          <td>狀態數</td>
          <td>2-3 個（idle / loading / result）</td>
          <td>4-6 個（idle / processing / success / error / disconnected&hellip;）</td>
      </tr>
      <tr>
          <td>退出路徑</td>
          <td>不需要（按鈕自己恢復）</td>
          <td>每個中間狀態都必須有退出路徑</td>
      </tr>
      <tr>
          <td>設計工具</td>
          <td>按鈕狀態檢查清單</td>
          <td><a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">畫面狀態矩陣（模組一）</a></td>
      </tr>
  </tbody>
</table>
<h3 id="設計模式">設計模式</h3>
<p>以連線流程為例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[idle] ─點擊連線→ [connecting] ─成功→ [connected]
</span></span><span class="line"><span class="ln">2</span><span class="cl">                       │                    │
</span></span><span class="line"><span class="ln">3</span><span class="cl">                       ├─失敗→ [error]       ├─斷線→ [disconnected]
</span></span><span class="line"><span class="ln">4</span><span class="cl">                       └─取消→ [idle]        └─手動斷開→ [idle]</span></span></code></pre></div><p>每個狀態的回饋設計：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>UI 呈現</th>
          <th>可用操作</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idle</td>
          <td>連線按鈕</td>
          <td>點擊連線</td>
          <td>返回上一頁</td>
      </tr>
      <tr>
          <td>connecting</td>
          <td>全頁 spinner + 「連線中」文字</td>
          <td>取消</td>
          <td>取消 → idle</td>
      </tr>
      <tr>
          <td>connected</td>
          <td>功能畫面</td>
          <td>使用功能 / 斷開</td>
          <td>斷開 → idle / 返回</td>
      </tr>
      <tr>
          <td>error</td>
          <td>錯誤訊息 + 重連按鈕</td>
          <td>重連 / 返回</td>
          <td>重連 → connecting / 返回 → idle</td>
      </tr>
      <tr>
          <td>disconnected</td>
          <td>「連線中斷」+ 重連按鈕</td>
          <td>重連 / 返回</td>
          <td>重連 → connecting / 返回 → idle</td>
      </tr>
  </tbody>
</table>
<p><strong>關鍵設計原則</strong>：error 和 disconnected 必須同時提供「重連」和「返回」兩條路徑。只有重連會把使用者鎖在錯誤迴圈（如果問題無法透過重連解決，使用者永遠出不去）。</p>
<p>timeout 是 connecting 的隱形退出路徑：取消靠使用者主動，timeout 是系統兜底 — 取值略大於 client / 後端的逾時設定（讓真正的失敗先回來、而非 UI 提前放棄），逾時後轉入 error 狀態走「重連 / 返回」。</p>
<h3 id="畫面級回饋的檢查清單">畫面級回饋的檢查清單</h3>
<ul>
<li><input disabled="" type="checkbox"> 每個中間狀態能被使用者區辨（獨立畫面、或同畫面上明確的狀態指示）？</li>
<li><input disabled="" type="checkbox"> 每個中間狀態有退出路徑（對應<a href="/blog/ux-design/01-screen-state-machine/" data-link-title="模組一：畫面狀態機設計" data-link-desc="畫面狀態矩陣（顯示 / 操作 / 進入 / 退出）— 退出路徑為空 = UX 死胡同">畫面狀態矩陣</a>）？</li>
<li><input disabled="" type="checkbox"> 等待狀態提供取消操作？</li>
<li><input disabled="" type="checkbox"> 錯誤 / 中斷狀態提供重試<strong>和</strong>返回兩條路徑？</li>
<li><input disabled="" type="checkbox"> 長時間等待有 timeout 機制（不會永久卡在 connecting）？</li>
</ul>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>問題</th>
          <th>使用者行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>按鈕無任何視覺回饋</td>
          <td>使用者不確定有沒有按到</td>
          <td>重複點擊</td>
      </tr>
      <tr>
          <td>非同步操作不禁用按鈕</td>
          <td>loading 期間仍可點擊</td>
          <td>重複提交（雙重扣款、重複建立）</td>
      </tr>
      <tr>
          <td>loading 結束不恢復按鈕</td>
          <td>操作完成但按鈕仍 disabled</td>
          <td>使用者以為介面當掉</td>
      </tr>
      <tr>
          <td>只有 loading 無結果通知</td>
          <td>spinner 消失但畫面沒變化</td>
          <td>使用者不確定操作是否成功</td>
      </tr>
      <tr>
          <td>同步按鈕無 debounce</td>
          <td>快速點擊觸發多次導航</td>
          <td>導航堆疊混亂（push 多次）</td>
      </tr>
      <tr>
          <td>畫面中間狀態無退出路徑</td>
          <td>使用者被卡在 connecting</td>
          <td>只能殺 app（UX 死胡同）</td>
      </tr>
      <tr>
          <td>錯誤狀態只有重連沒有返回</td>
          <td>問題無法重連解決時被鎖住</td>
          <td>在錯誤迴圈中反覆重連</td>
      </tr>
  </tbody>
</table>
<p>表格第一行（按鈕無任何視覺回饋）的完整實證案例見 <a href="/blog/ux-design/cases/export-button-zero-feedback/" data-link-title="U.C5 匯出按鈕按下零回饋 — 狀態機完備但 UI 沒接線" data-link-desc="Flutter app 匯出設定頁的確認匯出按鈕 onPressed 是空 callback，按下畫面毫無變化 — 使用者無法分辨匯出成功、進行中、還是功能根本沒做。ViewModel 的 idle/inProgress/completed/failed 狀態機早已完備，缺的只是頁面接線與三層回饋">U.C5 匯出按鈕零回饋</a>——狀態機完備但 UI 沒接線，三層回饋全缺。同族的兩個變體：佔位 handler 上線 — 按鈕存在但 onPressed 只接開發提示或 log，dev toast 讓開發自測「有反應」、掩蓋未接線（<a href="/blog/ux-design/cases/management-actions-placeholder-only/" data-link-title="U.C20 管理模式操作全是佔位 — dev toast 讓未接線看起來有反應" data-link-desc="UI 有按鈕、domain 層功能也寫完、按下去只有開發提示或 log：佔位 handler 比沒有按鈕更糟 — 按鈕的存在承諾功能存在，dev toast 讓開發自測「有反應」、掩蓋未接線">U.C20</a>）；回饋文字被版面擠壓 — state 有值、綁定正確，flex 寬度競爭把「已選擇: 3/45」壓成「&hellip;」（<a href="/blog/ux-design/cases/selection-count-layout-starvation/" data-link-title="U.C19 「已選擇」計數被版面擠壓成省略號 — 回饋死在 layout" data-link-desc="狀態文字顯示成「...」、資料層查起來卻一切正常 — 回饋鏈的最後一哩是版面，flex 寬度競爭把關鍵計數整串壓成省略號，state 正確、使用者拿到零資訊">U.C19</a>）。回饋鏈要驗到使用者的眼睛，state 有值、widget 有綁都不是終點。</p>
<p>版面擠壓類問題有一個常見的誤歸因：專案用了等比縮放套件（如 flutter_screenutil），版面出事時第一反應是查套件設定。換算工具工作在常數層（設計稿座標 → 裝置座標的數值換算）、空間分配發生在 layout 協商層（有限寬度分給動態內容）— 兩層獨立，「有用響應式套件」不構成版面不會擠壓的保證。判斷式（換算錯 vs 分配錯）與三層修法（設計層空間競爭規格 / 實作層 flex 顯式決策 / 驗證層窄幕檢查）見 <a href="/blog/report/proportional-scaling-is-not-space-allocation/" data-link-title="等比縮放不管空間分配：用了 sizing 套件、版面仍會擠壓" data-link-desc="用了響應式 / 等比縮放套件、版面仍然溢出或內容被壓縮，懷疑套件失效時使用。換算工具工作在常數層、空間分配發生在 layout 協商層，兩層獨立 — 工具靜默不等於該層無問題，「有用 X 套件」不構成該維度的品質保證">#228 等比縮放不管空間分配</a>。</p>
<h2 id="設計檢查清單">設計檢查清單</h2>
<p>為每個按鈕逐一確認：</p>
<ul>
<li><input disabled="" type="checkbox"> 點擊瞬間有視覺回饋（ripple / 狀態變化 / 觸覺）？</li>
<li><input disabled="" type="checkbox"> 非同步操作：按鈕進入 disabled + loading 狀態？</li>
<li><input disabled="" type="checkbox"> 非同步操作：完成後按鈕恢復 idle？</li>
<li><input disabled="" type="checkbox"> 操作結果有明確通知（成功 / 失敗 / 空）？</li>
<li><input disabled="" type="checkbox"> 失敗時提供可執行的下一步（重試 / 替代方案）？</li>
<li><input disabled="" type="checkbox"> 同步按鈕有 debounce 防連點？</li>
</ul>
<h2 id="參考來源">參考來源</h2>
<p>本篇整理的設計原則來自以下公開可用性研究與設計系統：</p>
<ul>
<li><strong>Nielsen&rsquo;s 10 Usability Heuristics</strong>（Nielsen Norman Group, 1994）— 啟發法 #1「系統狀態可見性」是回饋設計的理論基礎</li>
<li><strong>Doherty Threshold</strong>（Doherty &amp; Thadani, IBM 技術報告, 1982）— 400ms 回應時間門檻的原始研究</li>
<li><strong>Jakob Nielsen&rsquo;s Response Time Limits</strong>（1993）— 100ms / 1s / 10s 三門檻模型</li>
<li><strong>Material Design 3: Interaction States</strong>（Google, <a href="https://m3.material.io/foundations/interaction/states">m3.material.io</a>）— 按鈕互動狀態定義（loading indicator 是 M3 的獨立元件、引用見<a href="../response-time-strategy/">時間感知與回應策略</a>）</li>
<li><strong>Apple Human Interface Guidelines: Feedback</strong>（Apple Developer Documentation）— 回饋要可察覺、資訊明確並準確反映進度（本系列據此精神歸納出「即時、誠實的回應」的說法）</li>
<li><strong>Laws of UX</strong>（Jon Yablonski, <a href="https://lawsofux.com/doherty-threshold/">lawsofux.com</a>）— Doherty Threshold 的現代整理與應用案例</li>
</ul>
]]></content:encoded></item><item><title>按鈕狀態設計：一個按鈕的完整生命週期</title><link>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/button-state-design/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/button-state-design/</guid><description>&lt;h2 id="核心觀念">核心觀念&lt;/h2>
&lt;p>&lt;strong>按鈕的每種視覺狀態都傳達一種系統訊息&lt;/strong>，缺少任何一種，使用者就失去對應的判斷依據。&lt;a href="../feedback-three-layers/">三層回饋模型&lt;/a>回答「什麼時候給回饋」，本篇回答「用什麼視覺狀態給回饋」— 從無互動、游標懸停、鍵盤焦點到處理中，一顆按鈕的生命週期裡每個階段都有它要對使用者說的話。&lt;/p>
&lt;h2 id="基本狀態">基本狀態&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態&lt;/th>
 &lt;th>觸發條件&lt;/th>
 &lt;th>傳達訊息&lt;/th>
 &lt;th>必要性&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Default&lt;/td>
 &lt;td>無互動&lt;/td>
 &lt;td>「可以按我」&lt;/td>
 &lt;td>必要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hover&lt;/td>
 &lt;td>游標懸停（桌面端）&lt;/td>
 &lt;td>「這是可互動的元件」&lt;/td>
 &lt;td>桌面端必要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Active / Pressed&lt;/td>
 &lt;td>點擊中（手指按住）&lt;/td>
 &lt;td>「系統收到點擊了」&lt;/td>
 &lt;td>必要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Focus&lt;/td>
 &lt;td>鍵盤 Tab 導航選中&lt;/td>
 &lt;td>「目前焦點在這裡」&lt;/td>
 &lt;td>無障礙必要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Disabled&lt;/td>
 &lt;td>前置條件未滿足&lt;/td>
 &lt;td>「現在不能按」&lt;/td>
 &lt;td>必要&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Loading&lt;/td>
 &lt;td>非同步操作處理中&lt;/td>
 &lt;td>「正在處理，請等待」&lt;/td>
 &lt;td>非同步按鈕必要&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>前五種狀態出自 NN/g 與 Material Design 的互動狀態定義；Loading 則來自非同步按鈕的回饋需求（等待指示 + 防重複提交）— 本篇把它跟五個通用狀態並列，因為缺了它、非同步按鈕的生命週期就不完整。切換型控件（toggle、segmented control）另有 selected 態 —「現在是開還是關」也是一種系統訊息；本表聚焦動作按鈕，選取態屬選擇控件設計。&lt;/p>
&lt;h3 id="default">Default&lt;/h3>
&lt;p>按鈕的初始狀態（&lt;a href="../feedback-three-layers/">三層回饋模型&lt;/a>的狀態流程圖以 &lt;code>idle&lt;/code> 標記的就是這個狀態）。設計重點：讓使用者一眼辨識「這是個按鈕」。常見設計手法包括填色背景、邊框、圓角矩形、陰影。與普通文字或裝飾元素的區別必須明確。&lt;/p>
&lt;h3 id="hover桌面端">Hover（桌面端）&lt;/h3>
&lt;p>游標移到按鈕上方時的狀態。行動裝置沒有 hover 概念（手指碰到螢幕就是點擊），這個狀態僅對桌面端（含 web）有意義。常見做法是背景色稍微加深或加上微陰影。&lt;/p>
&lt;h3 id="active--pressed">Active / Pressed&lt;/h3>
&lt;p>使用者按住按鈕瞬間的狀態。對應三層回饋模型的第一層「點擊確認」。視覺上通常表現為凹陷效果、顏色加深或縮小動畫。在觸控裝置上可搭配觸覺回饋（震動）。&lt;/p>
&lt;h3 id="focus">Focus&lt;/h3>
&lt;p>透過鍵盤 Tab 鍵導航到按鈕時的狀態。這是無障礙設計（Accessibility）的基本要求 — WCAG 2.1 要求所有可互動元素在獲得焦點時必須有可見的視覺指示。常見做法是加上明顯的邊框或外框光暈。&lt;/p>
&lt;h3 id="disabled">Disabled&lt;/h3>
&lt;p>按鈕不可操作時的狀態。設計要點：&lt;/p>
&lt;ul>
&lt;li>視覺上與可操作狀態有明確區別（通常降低透明度）&lt;/li>
&lt;li>&lt;strong>告知使用者「為什麼不能按」&lt;/strong> — 單純灰掉不夠。優先用常駐說明文字（按鈕旁的條件提示）；tooltip 只在桌面端可行 — 行動端沒有 hover、disabled 元素通常也不接收 focus，hover 觸發的提示對行動端與鍵盤使用者不可達&lt;/li>
&lt;li>disabled 與隱藏的分工：條件可由使用者滿足時用 disabled（灰掉 + 說明怎麼解鎖，讓使用者知道功能存在）；條件與使用者無關（權限不足、功能未開通且無法自行開通）時考慮隱藏 — 展示一個永遠按不了的按鈕只製造困惑&lt;/li>
&lt;/ul>
&lt;h3 id="loading">Loading&lt;/h3>
&lt;p>非同步操作處理中的狀態。對應三層回饋模型的第二層「等待指示」。設計要點：&lt;/p>
&lt;ul>
&lt;li>按鈕內容替換為 spinner 或文字變更（「送出」→「送出中&amp;hellip;」）&lt;/li>
&lt;li>按鈕自動進入 disabled（防重複提交）&lt;/li>
&lt;li>操作完成後必須恢復到 default 或顯示結果&lt;/li>
&lt;li>設定逾時：取值略大於 client / 後端 timeout，逾時後恢復並給錯誤訊息（兜底原則同&lt;a href="../feedback-three-layers/">三層回饋模型&lt;/a>畫面級的 timeout 段）&lt;/li>
&lt;/ul>
&lt;h2 id="非同步按鈕的狀態流">非同步按鈕的狀態流&lt;/h2>
&lt;p>非同步按鈕＝點擊後需要等待伺服器或 IO 回應的按鈕（送出表單、呼叫 API）；同步按鈕＝操作在本地立即完成的按鈕（導航、切換）。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[Default] ─點擊→ [Active] ─釋放→ [Loading] ─完成→ [Default + 結果通知]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> └─失敗→ [Default + 錯誤訊息]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="要點">要點&lt;/h3>
&lt;ol>
&lt;li>&lt;strong>Active → Loading 的過渡&lt;/strong> 在使用者釋放點擊後立即發生（不等伺服器回應）&lt;/li>
&lt;li>&lt;strong>Loading 期間按鈕 disabled&lt;/strong> — 這是設計需求不是實作細節&lt;/li>
&lt;li>&lt;strong>Loading → Default 必須發生&lt;/strong> — 不論成功失敗，按鈕必須恢復可操作狀態&lt;/li>
&lt;li>&lt;strong>結果通知獨立於按鈕&lt;/strong> — 成功/失敗訊息用 SnackBar 或畫面更新呈現（形式選擇見&lt;a href="../notification-pattern-selection/">通知模式選擇&lt;/a>），不塞在按鈕裡&lt;/li>
&lt;/ol>
&lt;h2 id="同步按鈕的狀態流">同步按鈕的狀態流&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[Default] ─點擊→ [Active] ─釋放→ [Default]（操作在釋放時同步完成）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同步按鈕不需要 Loading 狀態。防連點用 &lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/debounce/" data-link-title="Debounce（防連點）" data-link-desc="說明合併短時間內重複觸發的技巧，以及 leading-edge 與 trailing-edge 兩種執行語意在按鈕防連點上的差別">debounce&lt;/a> 處理，不用 disabled — disabled 會讓按鈕在每次點擊後閃跳灰態，而第一次點擊本來就該立即生效；「立即執行、短時間內忽略後續點擊」的執行語意，&lt;a href="../feedback-three-layers/">三層回饋模型&lt;/a>的防連點段有完整說明。&lt;/p>
&lt;h2 id="落點設計系統元件層">落點：設計系統元件層&lt;/h2>
&lt;p>這些狀態的實作落點在共用 Button 元件層，不是逐顆按鈕。Material、Cupertino 等平台元件庫已內建 hover / pressed / focus 的視覺處理，真正要補的通常只有 Loading 與 Disabled 的語意 — 何時進入、何時恢復、disabled 原因怎麼給。估工時以「元件層一次工 + 各按鈕接上狀態來源」計算，而非「按鈕數 × 狀態數」— 後者會把成本高估數倍，讓完整的狀態設計看起來做不起。&lt;/p>
&lt;h2 id="元件語意與版面">元件語意與版面&lt;/h2>
&lt;p>狀態清單之外，按鈕的文字、圖示、選中視覺與所在的版面各自承載語意，常見缺口：&lt;/p>
&lt;p>&lt;strong>切換元件的標籤要能區分「現態」與「動作」&lt;/strong>。「顯示目標模式」（播放鍵顯示「播放」）和「顯示當前模式」（開關顯示現態）是兩種並存慣例，單顆文字按鈕無法自證是哪種 — 名詞標籤（「管理模式」）特別容易被讀成狀態指示。消歧做法：狀態顯示與動作按鈕分離（非互動的現態文字 +「切換模式」動作鈕）、或改用自帶 on/off 語意的控件（switch / segmented control）。不幫助消歧的圖示是雜訊、應移除（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/toggle-button-label-state-ambiguity/" data-link-title="U.C15 切換按鈕顯示目標模式被讀成當前狀態 — 標籤語意歧義" data-link-desc="模式切換按鈕的文字被使用者讀成「現在的狀態」而非「按了會去哪」— 單顆文字按鈕無法自證標籤是現態還是目標，歧義是結構性的，解法是把狀態顯示與切換動作的責任拆開">U.C15&lt;/a>）。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心觀念">核心觀念</h2>
<p><strong>按鈕的每種視覺狀態都傳達一種系統訊息</strong>，缺少任何一種，使用者就失去對應的判斷依據。<a href="../feedback-three-layers/">三層回饋模型</a>回答「什麼時候給回饋」，本篇回答「用什麼視覺狀態給回饋」— 從無互動、游標懸停、鍵盤焦點到處理中，一顆按鈕的生命週期裡每個階段都有它要對使用者說的話。</p>
<h2 id="基本狀態">基本狀態</h2>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>觸發條件</th>
          <th>傳達訊息</th>
          <th>必要性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Default</td>
          <td>無互動</td>
          <td>「可以按我」</td>
          <td>必要</td>
      </tr>
      <tr>
          <td>Hover</td>
          <td>游標懸停（桌面端）</td>
          <td>「這是可互動的元件」</td>
          <td>桌面端必要</td>
      </tr>
      <tr>
          <td>Active / Pressed</td>
          <td>點擊中（手指按住）</td>
          <td>「系統收到點擊了」</td>
          <td>必要</td>
      </tr>
      <tr>
          <td>Focus</td>
          <td>鍵盤 Tab 導航選中</td>
          <td>「目前焦點在這裡」</td>
          <td>無障礙必要</td>
      </tr>
      <tr>
          <td>Disabled</td>
          <td>前置條件未滿足</td>
          <td>「現在不能按」</td>
          <td>必要</td>
      </tr>
      <tr>
          <td>Loading</td>
          <td>非同步操作處理中</td>
          <td>「正在處理，請等待」</td>
          <td>非同步按鈕必要</td>
      </tr>
  </tbody>
</table>
<p>前五種狀態出自 NN/g 與 Material Design 的互動狀態定義；Loading 則來自非同步按鈕的回饋需求（等待指示 + 防重複提交）— 本篇把它跟五個通用狀態並列，因為缺了它、非同步按鈕的生命週期就不完整。切換型控件（toggle、segmented control）另有 selected 態 —「現在是開還是關」也是一種系統訊息；本表聚焦動作按鈕，選取態屬選擇控件設計。</p>
<h3 id="default">Default</h3>
<p>按鈕的初始狀態（<a href="../feedback-three-layers/">三層回饋模型</a>的狀態流程圖以 <code>idle</code> 標記的就是這個狀態）。設計重點：讓使用者一眼辨識「這是個按鈕」。常見設計手法包括填色背景、邊框、圓角矩形、陰影。與普通文字或裝飾元素的區別必須明確。</p>
<h3 id="hover桌面端">Hover（桌面端）</h3>
<p>游標移到按鈕上方時的狀態。行動裝置沒有 hover 概念（手指碰到螢幕就是點擊），這個狀態僅對桌面端（含 web）有意義。常見做法是背景色稍微加深或加上微陰影。</p>
<h3 id="active--pressed">Active / Pressed</h3>
<p>使用者按住按鈕瞬間的狀態。對應三層回饋模型的第一層「點擊確認」。視覺上通常表現為凹陷效果、顏色加深或縮小動畫。在觸控裝置上可搭配觸覺回饋（震動）。</p>
<h3 id="focus">Focus</h3>
<p>透過鍵盤 Tab 鍵導航到按鈕時的狀態。這是無障礙設計（Accessibility）的基本要求 — WCAG 2.1 要求所有可互動元素在獲得焦點時必須有可見的視覺指示。常見做法是加上明顯的邊框或外框光暈。</p>
<h3 id="disabled">Disabled</h3>
<p>按鈕不可操作時的狀態。設計要點：</p>
<ul>
<li>視覺上與可操作狀態有明確區別（通常降低透明度）</li>
<li><strong>告知使用者「為什麼不能按」</strong> — 單純灰掉不夠。優先用常駐說明文字（按鈕旁的條件提示）；tooltip 只在桌面端可行 — 行動端沒有 hover、disabled 元素通常也不接收 focus，hover 觸發的提示對行動端與鍵盤使用者不可達</li>
<li>disabled 與隱藏的分工：條件可由使用者滿足時用 disabled（灰掉 + 說明怎麼解鎖，讓使用者知道功能存在）；條件與使用者無關（權限不足、功能未開通且無法自行開通）時考慮隱藏 — 展示一個永遠按不了的按鈕只製造困惑</li>
</ul>
<h3 id="loading">Loading</h3>
<p>非同步操作處理中的狀態。對應三層回饋模型的第二層「等待指示」。設計要點：</p>
<ul>
<li>按鈕內容替換為 spinner 或文字變更（「送出」→「送出中&hellip;」）</li>
<li>按鈕自動進入 disabled（防重複提交）</li>
<li>操作完成後必須恢復到 default 或顯示結果</li>
<li>設定逾時：取值略大於 client / 後端 timeout，逾時後恢復並給錯誤訊息（兜底原則同<a href="../feedback-three-layers/">三層回饋模型</a>畫面級的 timeout 段）</li>
</ul>
<h2 id="非同步按鈕的狀態流">非同步按鈕的狀態流</h2>
<p>非同步按鈕＝點擊後需要等待伺服器或 IO 回應的按鈕（送出表單、呼叫 API）；同步按鈕＝操作在本地立即完成的按鈕（導航、切換）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[Default] ─點擊→ [Active] ─釋放→ [Loading] ─完成→ [Default + 結果通知]
</span></span><span class="line"><span class="ln">2</span><span class="cl">                                      │
</span></span><span class="line"><span class="ln">3</span><span class="cl">                                      └─失敗→ [Default + 錯誤訊息]</span></span></code></pre></div><h3 id="要點">要點</h3>
<ol>
<li><strong>Active → Loading 的過渡</strong> 在使用者釋放點擊後立即發生（不等伺服器回應）</li>
<li><strong>Loading 期間按鈕 disabled</strong> — 這是設計需求不是實作細節</li>
<li><strong>Loading → Default 必須發生</strong> — 不論成功失敗，按鈕必須恢復可操作狀態</li>
<li><strong>結果通知獨立於按鈕</strong> — 成功/失敗訊息用 SnackBar 或畫面更新呈現（形式選擇見<a href="../notification-pattern-selection/">通知模式選擇</a>），不塞在按鈕裡</li>
</ol>
<h2 id="同步按鈕的狀態流">同步按鈕的狀態流</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[Default] ─點擊→ [Active] ─釋放→ [Default]（操作在釋放時同步完成）</span></span></code></pre></div><p>同步按鈕不需要 Loading 狀態。防連點用 <a href="/blog/ux-design/knowledge-cards/debounce/" data-link-title="Debounce（防連點）" data-link-desc="說明合併短時間內重複觸發的技巧，以及 leading-edge 與 trailing-edge 兩種執行語意在按鈕防連點上的差別">debounce</a> 處理，不用 disabled — disabled 會讓按鈕在每次點擊後閃跳灰態，而第一次點擊本來就該立即生效；「立即執行、短時間內忽略後續點擊」的執行語意，<a href="../feedback-three-layers/">三層回饋模型</a>的防連點段有完整說明。</p>
<h2 id="落點設計系統元件層">落點：設計系統元件層</h2>
<p>這些狀態的實作落點在共用 Button 元件層，不是逐顆按鈕。Material、Cupertino 等平台元件庫已內建 hover / pressed / focus 的視覺處理，真正要補的通常只有 Loading 與 Disabled 的語意 — 何時進入、何時恢復、disabled 原因怎麼給。估工時以「元件層一次工 + 各按鈕接上狀態來源」計算，而非「按鈕數 × 狀態數」— 後者會把成本高估數倍，讓完整的狀態設計看起來做不起。</p>
<h2 id="元件語意與版面">元件語意與版面</h2>
<p>狀態清單之外，按鈕的文字、圖示、選中視覺與所在的版面各自承載語意，常見缺口：</p>
<p><strong>切換元件的標籤要能區分「現態」與「動作」</strong>。「顯示目標模式」（播放鍵顯示「播放」）和「顯示當前模式」（開關顯示現態）是兩種並存慣例，單顆文字按鈕無法自證是哪種 — 名詞標籤（「管理模式」）特別容易被讀成狀態指示。消歧做法：狀態顯示與動作按鈕分離（非互動的現態文字 +「切換模式」動作鈕）、或改用自帶 on/off 語意的控件（switch / segmented control）。不幫助消歧的圖示是雜訊、應移除（<a href="/blog/ux-design/cases/toggle-button-label-state-ambiguity/" data-link-title="U.C15 切換按鈕顯示目標模式被讀成當前狀態 — 標籤語意歧義" data-link-desc="模式切換按鈕的文字被使用者讀成「現在的狀態」而非「按了會去哪」— 單顆文字按鈕無法自證標籤是現態還是目標，歧義是結構性的，解法是把狀態顯示與切換動作的責任拆開">U.C15</a>）。</p>
<p><strong>非互動指示不可與動作按鈕同形</strong>。狀態圖示混排在動作按鈕同列、又用近似圖形（描邊 vs 實心）時，使用者的預設是整列可點 — 點到指示的「沒反應」被讀成按鈕壞掉。描邊 / 實心差異不是可靠的互動性訊號；指示要用明顯非按鈕的形態（文字 chip、圓點）或移出動作列，同一個圖形全畫面只承載一個語意（<a href="/blog/ux-design/cases/status-icon-mistaken-for-button/" data-link-title="U.C18 狀態圖示被當成按鈕點 — 非互動指示與動作按鈕同形" data-link-desc="使用者回報「某個按鈕點了沒反應」、查程式發現那不是按鈕時使用。非互動的狀態圖示與動作按鈕同列混排且形態近似（描邊 vs 實心），使用者無法區分可點性">U.C18</a>）。</p>
<p><strong>選中態是（底色, 文字色）的成對設計</strong>。只設定選中底色、文字色走主題預設，組合對比沒有人驗證過 — 淺藍選中底疊淺色預設文字就是「選中後看不到字」的成因。對比方向成對反轉：底色淺配深字、底色深配淺字，逐狀態驗 WCAG AA（一般字級 4.5:1、大字 3:1）（<a href="/blog/ux-design/cases/selected-chip-contrast-not-paired/" data-link-title="U.C17 選中態換底色不換文字色 — 對比沒有成對設計" data-link-desc="選中的 chip / tab / 按鈕文字看不清楚：選中態是「底色 × 文字色」的成對設計，只指定其中一半、另一半走元件庫預設，組合對比從未被驗證">U.C17</a>）。</p>
<p><strong>水平排列的元件列溢出時要有捲動 <a href="/blog/ux-design/knowledge-cards/affordance/" data-link-title="Affordance（操作暗示）" data-link-desc="說明介面元素透過外觀傳達「可以對它做什麼」的訊號 — 可點、可捲、可拖的暗示與實際行為對齊時介面才可預測">affordance</a></strong>。chip 列、按鈕列超出畫面寬度時，截斷的預設讀法是「壞了」或「被旁邊的元件遮住」、不是「可以捲」— 提示手段：邊緣漸層遮罩、下一個項目部分露出、方向箭頭、捲軸指示；選項少時優先消除溢出（wrap 換行、改選單）。截斷邊緣緊貼固定按鈕會讓視覺歸因錯到那顆按鈕上 — 一條實作正確的可捲動篩選列因此在驗收被回報成「被重新整理按鈕遮蔽」（<a href="/blog/ux-design/cases/filter-chips-overflow-no-affordance/" data-link-title="U.C16 篩選列截斷被讀成遮蔽 — 水平溢出沒有捲動提示" data-link-desc="水平清單超出畫面寬度、使用者回報「被別的元件蓋住」或找不到後面的選項時使用。截斷若無 affordance（漸層、部分露出、箭頭），使用者讀到的是「壞了」不是「可以捲」">U.C16</a>）。</p>
<h2 id="常見設計失誤">常見設計失誤</h2>
<table>
  <thead>
      <tr>
          <th>失誤</th>
          <th>後果</th>
          <th>修正</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Loading 狀態未禁用按鈕</td>
          <td>重複提交</td>
          <td>Loading 自動帶入 disabled</td>
      </tr>
      <tr>
          <td>Disabled 沒有說明原因</td>
          <td>使用者困惑「為什麼不能按」</td>
          <td>搭配 tooltip 或條件說明</td>
      </tr>
      <tr>
          <td>Focus 狀態不可見</td>
          <td>鍵盤使用者無法導航</td>
          <td>加明顯 focus ring</td>
      </tr>
      <tr>
          <td>Active 狀態與 Default 視覺差異太小</td>
          <td>觸控裝置上看不出「有按到」</td>
          <td>加大視覺差異或加觸覺回饋</td>
      </tr>
      <tr>
          <td>按鈕長期 Loading 無超時機制</td>
          <td>介面永久凍結</td>
          <td>設定逾時，逾時後恢復 + 錯誤訊息</td>
      </tr>
  </tbody>
</table>
<h2 id="設計檢查清單">設計檢查清單</h2>
<p>逐一確認每個按鈕：</p>
<ul>
<li><input disabled="" type="checkbox"> 有 Default 狀態且明確可辨識為按鈕？</li>
<li><input disabled="" type="checkbox"> 桌面端有 Hover 狀態？</li>
<li><input disabled="" type="checkbox"> 有 Active / Pressed 視覺回饋？</li>
<li><input disabled="" type="checkbox"> 有可見的 Focus 狀態（鍵盤導航）？</li>
<li><input disabled="" type="checkbox"> Disabled 狀態有說明原因？</li>
<li><input disabled="" type="checkbox"> 非同步按鈕有 Loading 狀態？</li>
<li><input disabled="" type="checkbox"> Loading 期間按鈕 disabled？</li>
<li><input disabled="" type="checkbox"> Loading 有超時機制（不會永久卡住）？</li>
<li><input disabled="" type="checkbox"> Loading 結束後恢復可操作狀態？</li>
<li><input disabled="" type="checkbox"> 切換型按鈕的文字讀起來是動作而非現態（或已拆成狀態顯示 + 動作按鈕）？</li>
<li><input disabled="" type="checkbox"> 同列的非互動指示與動作按鈕形態可區分、圖示無撞名？</li>
<li><input disabled="" type="checkbox"> Selected 態的底色與文字色成對設計、對比達 WCAG AA？</li>
</ul>
<h2 id="參考來源">參考來源</h2>
<ul>
<li><strong>Nielsen Norman Group: &ldquo;Button States: Communicate Interaction&rdquo;</strong> — 按鈕各狀態如何傳達互動性的使用性研究</li>
<li><strong>Material Design 3: States</strong>（<a href="https://m3.material.io/foundations/interaction/states">m3.material.io/foundations/interaction/states</a>）— Google 設計系統的互動狀態定義</li>
<li><strong>WCAG 2.1 Success Criterion 2.4.7: Focus Visible</strong> — 焦點狀態可見性的無障礙標準</li>
<li><strong>Apple Human Interface Guidelines: Buttons</strong> — Apple 的按鈕設計原則</li>
</ul>
]]></content:encoded></item><item><title>時間感知與回應策略：Loading 的形式由等待時間門檻決定</title><link>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/response-time-strategy/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/response-time-strategy/</guid><description>&lt;h2 id="核心觀念">核心觀念&lt;/h2>
&lt;p>&lt;strong>回饋的形式取決於等待時間&lt;/strong> — loading spinner 只適合中間帶的等待。100ms 的頁面切換和 30 秒的批次匯出需要的指示完全不同：前者加 spinner 是視覺雜訊、讓操作顯得比實際慢，後者只有 spinner 撐不住使用者的耐心、他需要知道還要等多久。&lt;/p>
&lt;p>設計回饋策略的依據是人類對時間的主觀感知 — 不同的等待長度會觸發不同的心理反應。本篇展開&lt;a href="../feedback-three-layers/">三層回饋模型&lt;/a>第二層「等待指示」（三層＝點擊確認 / 等待指示 / 結果通知）的策略選擇。&lt;/p>
&lt;h2 id="時間門檻">時間門檻&lt;/h2>
&lt;p>這些門檻來自兩個量測對象不同的研究框架：100ms / 1s / 10s 是感知與注意力門檻（Miller、Nielsen）、400ms 是生產力門檻（&lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/doherty-threshold/" data-link-title="Doherty Threshold（400ms 門檻）" data-link-desc="說明 400ms 回應時間門檻的出身（IBM 生產力研究）、現代設計慣例的轉譯過程，以及它跟 Nielsen 感知門檻的量測差異">Doherty&lt;/a>）。本篇把兩者合併在同一條時間軸上使用 — 設計決策關心的是「等多久該給什麼回饋」，兩個框架在這個問題上給出互補的分段點。&lt;/p>
&lt;h3 id="100ms--即時感知門檻">100ms — 即時感知門檻&lt;/h3>
&lt;p>來源：人類感知研究（Miller, 1968; Card, Moran &amp;amp; Newell, 1983）。&lt;/p>
&lt;p>100ms 以內的延遲被感知為「即時」。使用者不會注意到任何等待，操作感覺像是直接發生。&lt;/p>
&lt;p>&lt;strong>設計策略&lt;/strong>：第一層的點擊確認（視覺狀態變化）就足夠，無需額外等待指示。&lt;/p>
&lt;p>&lt;strong>適用&lt;/strong>：同步操作（導航、切換、本地計算）。&lt;/p>
&lt;h3 id="400ms--doherty-threshold">400ms — Doherty Threshold&lt;/h3>
&lt;p>來源：Doherty &amp;amp; Thadani, &amp;ldquo;The Economic Value of Rapid Response Time&amp;rdquo;（IBM 技術報告, 1982）。&lt;/p>
&lt;p>原始研究發現：當電腦回應時間低於 400ms 時，使用者的操作效率大幅提升 — 人機互動進入一種「流暢循環」，雙方都不需要等對方。超過 400ms，這個循環被打斷。&lt;/p>
&lt;p>&lt;strong>設計策略&lt;/strong>：400ms 以內完成的操作可省略 loading indicator — 把生產力門檻讀成回饋門檻是 Laws of UX 等現代整理的轉譯、本篇沿用。可以用動畫過渡（fade in、slide）讓結果「出現」而非「替換」，利用動畫時間掩蓋等待感。&lt;/p>
&lt;p>&lt;strong>適用&lt;/strong>：快速 API 回應、本地資料庫查詢、簡單計算。&lt;/p>
&lt;h3 id="1-秒--心流維持門檻">1 秒 — 心流維持門檻&lt;/h3>
&lt;p>來源：Nielsen&amp;rsquo;s response time limits（Usability Engineering, 1993）。&lt;/p>
&lt;p>1 秒以內，使用者的思考流程不會被打斷。雖然能感知到延遲，但不會覺得「系統卡住了」。&lt;/p>
&lt;p>&lt;strong>設計策略&lt;/strong>：開始顯示輕量級等待指示。按鈕進入 loading 狀態（spinner 取代文字）。但不需要進度百分比或預估時間。&lt;/p>
&lt;p>&lt;strong>適用&lt;/strong>：一般 API 請求、檔案讀寫、中等複雜度查詢。&lt;/p>
&lt;h3 id="10-秒--注意力維持上限">10 秒 — 注意力維持上限&lt;/h3>
&lt;p>來源：Nielsen&amp;rsquo;s response time limits（同上）。&lt;/p>
&lt;p>超過 10 秒，使用者開始考慮離開。注意力已經從當前操作轉移到是否放棄的權衡上。&lt;/p>
&lt;p>&lt;strong>設計策略&lt;/strong>：必須提供進度指示（百分比 / 步驟計數 / 預估剩餘時間），讓使用者判斷「還值不值得等」。考慮提供取消操作的選項。操作再長（數十秒以上）考慮轉為背景任務、完成後由通知系統告知 — 通知系統設計在本篇範圍外。&lt;/p>
&lt;p>&lt;strong>適用&lt;/strong>：檔案上傳、批次處理、大型資料同步。&lt;/p>
&lt;h2 id="策略對照表">策略對照表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>等待時間&lt;/th>
 &lt;th>使用者感受&lt;/th>
 &lt;th>回饋策略&lt;/th>
 &lt;th>範例&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>0-100ms&lt;/td>
 &lt;td>即時&lt;/td>
 &lt;td>點擊確認即可&lt;/td>
 &lt;td>頁面導航、開關切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>100-400ms&lt;/td>
 &lt;td>幾乎即時&lt;/td>
 &lt;td>動畫過渡掩蓋等待&lt;/td>
 &lt;td>快速搜尋、本地查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>400ms-1s&lt;/td>
 &lt;td>短暫等待&lt;/td>
 &lt;td>Spinner / loading 狀態&lt;/td>
 &lt;td>API 查詢、表單送出&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1-10s&lt;/td>
 &lt;td>明顯等待&lt;/td>
 &lt;td>進度指示 + spinner&lt;/td>
 &lt;td>檔案上傳、資料匯入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&amp;gt; 10s&lt;/td>
 &lt;td>長時間等待&lt;/td>
 &lt;td>進度百分比 + 預估時間 + 取消選項&lt;/td>
 &lt;td>批次處理、大型同步&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="門檻怎麼落地延遲是分布不是常數">門檻怎麼落地：延遲是分布不是常數&lt;/h2>
&lt;p>策略對照表把「等待時間」當成單一數值，但真實延遲是分布：同一支 API 平常 300ms，尖峰或弱網下 3s。發出請求前無法知道這一次會落在哪一帶，照表選策略需要兩個補充機制。&lt;/p>
&lt;p>&lt;strong>用高百分位對表，不用平均值&lt;/strong>：對表的統計量取 p90 或 p95（應用效能監控（APM）平台或瀏覽器 DevTools 的 Network 面板都查得到）。平均 300ms、p95 卻 2s 的操作，代表每二十次就有一次使用者盯著沒有任何指示的畫面等 2 秒 — 對表要照顧的是這些請求，不是平均值。&lt;/p>
&lt;h3 id="延遲顯示與最短顯示時間防-spinner-閃爍">延遲顯示與最短顯示時間：防 spinner 閃爍&lt;/h3>
&lt;p>&lt;strong>延遲顯示（delayed spinner）&lt;/strong>：發出請求後先不顯示 spinner，等 300-400ms 才掛上；請求先回來就完全不顯示。快的請求（多數）沒有 spinner 閃爍，慢的請求（少數）在使用者開始懷疑之前得到指示。這是延遲分布橫跨門檻帶時的標準解 — 判準從「這支 API 多快」變成「超過 400ms 才需要告訴使用者」。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心觀念">核心觀念</h2>
<p><strong>回饋的形式取決於等待時間</strong> — loading spinner 只適合中間帶的等待。100ms 的頁面切換和 30 秒的批次匯出需要的指示完全不同：前者加 spinner 是視覺雜訊、讓操作顯得比實際慢，後者只有 spinner 撐不住使用者的耐心、他需要知道還要等多久。</p>
<p>設計回饋策略的依據是人類對時間的主觀感知 — 不同的等待長度會觸發不同的心理反應。本篇展開<a href="../feedback-three-layers/">三層回饋模型</a>第二層「等待指示」（三層＝點擊確認 / 等待指示 / 結果通知）的策略選擇。</p>
<h2 id="時間門檻">時間門檻</h2>
<p>這些門檻來自兩個量測對象不同的研究框架：100ms / 1s / 10s 是感知與注意力門檻（Miller、Nielsen）、400ms 是生產力門檻（<a href="/blog/ux-design/knowledge-cards/doherty-threshold/" data-link-title="Doherty Threshold（400ms 門檻）" data-link-desc="說明 400ms 回應時間門檻的出身（IBM 生產力研究）、現代設計慣例的轉譯過程，以及它跟 Nielsen 感知門檻的量測差異">Doherty</a>）。本篇把兩者合併在同一條時間軸上使用 — 設計決策關心的是「等多久該給什麼回饋」，兩個框架在這個問題上給出互補的分段點。</p>
<h3 id="100ms--即時感知門檻">100ms — 即時感知門檻</h3>
<p>來源：人類感知研究（Miller, 1968; Card, Moran &amp; Newell, 1983）。</p>
<p>100ms 以內的延遲被感知為「即時」。使用者不會注意到任何等待，操作感覺像是直接發生。</p>
<p><strong>設計策略</strong>：第一層的點擊確認（視覺狀態變化）就足夠，無需額外等待指示。</p>
<p><strong>適用</strong>：同步操作（導航、切換、本地計算）。</p>
<h3 id="400ms--doherty-threshold">400ms — Doherty Threshold</h3>
<p>來源：Doherty &amp; Thadani, &ldquo;The Economic Value of Rapid Response Time&rdquo;（IBM 技術報告, 1982）。</p>
<p>原始研究發現：當電腦回應時間低於 400ms 時，使用者的操作效率大幅提升 — 人機互動進入一種「流暢循環」，雙方都不需要等對方。超過 400ms，這個循環被打斷。</p>
<p><strong>設計策略</strong>：400ms 以內完成的操作可省略 loading indicator — 把生產力門檻讀成回饋門檻是 Laws of UX 等現代整理的轉譯、本篇沿用。可以用動畫過渡（fade in、slide）讓結果「出現」而非「替換」，利用動畫時間掩蓋等待感。</p>
<p><strong>適用</strong>：快速 API 回應、本地資料庫查詢、簡單計算。</p>
<h3 id="1-秒--心流維持門檻">1 秒 — 心流維持門檻</h3>
<p>來源：Nielsen&rsquo;s response time limits（Usability Engineering, 1993）。</p>
<p>1 秒以內，使用者的思考流程不會被打斷。雖然能感知到延遲，但不會覺得「系統卡住了」。</p>
<p><strong>設計策略</strong>：開始顯示輕量級等待指示。按鈕進入 loading 狀態（spinner 取代文字）。但不需要進度百分比或預估時間。</p>
<p><strong>適用</strong>：一般 API 請求、檔案讀寫、中等複雜度查詢。</p>
<h3 id="10-秒--注意力維持上限">10 秒 — 注意力維持上限</h3>
<p>來源：Nielsen&rsquo;s response time limits（同上）。</p>
<p>超過 10 秒，使用者開始考慮離開。注意力已經從當前操作轉移到是否放棄的權衡上。</p>
<p><strong>設計策略</strong>：必須提供進度指示（百分比 / 步驟計數 / 預估剩餘時間），讓使用者判斷「還值不值得等」。考慮提供取消操作的選項。操作再長（數十秒以上）考慮轉為背景任務、完成後由通知系統告知 — 通知系統設計在本篇範圍外。</p>
<p><strong>適用</strong>：檔案上傳、批次處理、大型資料同步。</p>
<h2 id="策略對照表">策略對照表</h2>
<table>
  <thead>
      <tr>
          <th>等待時間</th>
          <th>使用者感受</th>
          <th>回饋策略</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0-100ms</td>
          <td>即時</td>
          <td>點擊確認即可</td>
          <td>頁面導航、開關切換</td>
      </tr>
      <tr>
          <td>100-400ms</td>
          <td>幾乎即時</td>
          <td>動畫過渡掩蓋等待</td>
          <td>快速搜尋、本地查詢</td>
      </tr>
      <tr>
          <td>400ms-1s</td>
          <td>短暫等待</td>
          <td>Spinner / loading 狀態</td>
          <td>API 查詢、表單送出</td>
      </tr>
      <tr>
          <td>1-10s</td>
          <td>明顯等待</td>
          <td>進度指示 + spinner</td>
          <td>檔案上傳、資料匯入</td>
      </tr>
      <tr>
          <td>&gt; 10s</td>
          <td>長時間等待</td>
          <td>進度百分比 + 預估時間 + 取消選項</td>
          <td>批次處理、大型同步</td>
      </tr>
  </tbody>
</table>
<h2 id="門檻怎麼落地延遲是分布不是常數">門檻怎麼落地：延遲是分布不是常數</h2>
<p>策略對照表把「等待時間」當成單一數值，但真實延遲是分布：同一支 API 平常 300ms，尖峰或弱網下 3s。發出請求前無法知道這一次會落在哪一帶，照表選策略需要兩個補充機制。</p>
<p><strong>用高百分位對表，不用平均值</strong>：對表的統計量取 p90 或 p95（應用效能監控（APM）平台或瀏覽器 DevTools 的 Network 面板都查得到）。平均 300ms、p95 卻 2s 的操作，代表每二十次就有一次使用者盯著沒有任何指示的畫面等 2 秒 — 對表要照顧的是這些請求，不是平均值。</p>
<h3 id="延遲顯示與最短顯示時間防-spinner-閃爍">延遲顯示與最短顯示時間：防 spinner 閃爍</h3>
<p><strong>延遲顯示（delayed spinner）</strong>：發出請求後先不顯示 spinner，等 300-400ms 才掛上；請求先回來就完全不顯示。快的請求（多數）沒有 spinner 閃爍，慢的請求（少數）在使用者開始懷疑之前得到指示。這是延遲分布橫跨門檻帶時的標準解 — 判準從「這支 API 多快」變成「超過 400ms 才需要告訴使用者」。</p>
<p><strong>最短顯示時間（minimum display time）</strong>：spinner 一旦顯示就至少停留一段時間（常見慣例值 300ms），請求提早完成也不立即移除，消除「spinner 閃一下就消失」的視覺雜訊。</p>
<p>延遲顯示的 300-400ms 與最短顯示的 300ms 都是業界慣例值、不是研究門檻，可依產品實際延遲分布調整。原則是顯示決策跟著使用者的感知走、不跟著單次請求的實際時長走。</p>
<table>
  <thead>
      <tr>
          <th>延遲分布</th>
          <th>策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>p90 &lt; 400ms</td>
          <td>延遲顯示 spinner（多數使用者不會看到它）</td>
      </tr>
      <tr>
          <td>p90 400ms - 1s</td>
          <td>按鈕 loading 狀態 + 最短顯示時間</td>
      </tr>
      <tr>
          <td>p90 1 - 10s</td>
          <td>進度指示</td>
      </tr>
      <tr>
          <td>跨帶（p50 與 p95 差一個帶以上）</td>
          <td>以 p95 所在帶設計 UI、加延遲顯示讓快的請求不受擾</td>
      </tr>
  </tbody>
</table>
<p>驗證方式：用瀏覽器 DevTools 的網速節流（或 proxy 限速）模擬慢回應，逐帶確認對應的 UI 真的會出現；沒有節流工具時，暫時在 client 加人工延遲也能達到同樣目的。</p>
<h2 id="感知效能技巧">感知效能技巧</h2>
<p>真實效能無法改變時（伺服器就是要跑 3 秒），可以透過設計降低使用者的等待感受：</p>
<h3 id="骨架畫面skeleton-screen">骨架畫面（Skeleton Screen）</h3>
<p>在資料載入前先顯示內容區域的灰色佔位形狀。使用者看到「畫面結構」比看到空白等待更不焦慮。適合列表、卡片等有明確版面的內容。佔位尺寸要與最終內容一致 — 載入完成瞬間的版面跳動（layout shift）比空白等待更傷體驗，會抵銷 skeleton 的全部收益。</p>
<h3 id="spinner-vs-skeleton形狀已知用-skeleton未知用-spinner">Spinner vs Skeleton：形狀已知用 skeleton、未知用 spinner</h3>
<p>Spinner 和 Skeleton Screen 都是等待指示，但適用場景不同。選錯會降低使用者體驗。</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>Spinner</th>
          <th>Skeleton Screen</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>結果形狀已知（列表 / 卡片 / 個人資料）</td>
          <td></td>
          <td>適合：佔位形狀接近最終結果，降低焦慮</td>
      </tr>
      <tr>
          <td>結果形狀不確定（連線結果 / 計算結果）</td>
          <td>適合：無法預畫佔位形狀</td>
          <td></td>
      </tr>
      <tr>
          <td>操作觸發的等待（按下按鈕後）</td>
          <td>適合：按鈕內 spinner</td>
          <td></td>
      </tr>
      <tr>
          <td>頁面首次載入（進入新畫面）</td>
          <td></td>
          <td>適合：給畫面結構預期</td>
      </tr>
      <tr>
          <td>等待時間短且可預測（&lt; 2s）</td>
          <td>適合：輕量指示</td>
          <td></td>
      </tr>
      <tr>
          <td>等待時間較長（2-10s）</td>
          <td></td>
          <td>適合：佔位結構降低空白等待的焦慮</td>
      </tr>
  </tbody>
</table>
<p><strong>判斷口訣</strong>：「知道長什麼樣 → skeleton；不知道 → spinner。」</p>
<p>表中三個條件軸（形狀、觸發方式、時長）可能同時命中不同建議，裁決順序是<strong>形狀 &gt; 觸發 &gt; 時長</strong>。例：按下按鈕載入個人資料、預期 1.5s — 形狀已知（個人資料版面固定）就用 skeleton，即使「操作觸發」那列寫 spinner。觸發方式與時長只在形狀軸分不出勝負（形狀部分已知、或混合內容）時才進入裁決。</p>
<p>表中 2s 的分界是切分用的慣例值、不在研究門檻系裡。1-10s 帶跟策略對照表的「進度指示」建議並不衝突 — 內容載入走 skeleton、操作進度走進度指示，正是形狀軸裁決的結果。</p>
<h3 id="樂觀更新optimistic-update">樂觀更新（Optimistic Update）</h3>
<p>在伺服器回應前先更新 UI，假設操作會成功。如果失敗再回滾。適合高成功率的操作（按讚、收藏、勾選）。風險：失敗回滾時，使用者看到剛完成的操作被悄悄還原、會開始不信任介面 — 回滾因此要走<a href="../feedback-three-layers/">三層回饋模型</a>的失敗通知：明確告知「操作未成功、已還原」加上下一步（重試），讓還原是被告知的結果、不是無聲消失。</p>
<h3 id="漸進式載入progressive-loading">漸進式載入（Progressive Loading）</h3>
<p>先載入最重要的內容（文字），再載入次要內容（圖片、評論）。使用者可以在等待期間開始閱讀，降低「什麼都不能做」的焦慮。</p>
<h3 id="動畫過渡">動畫過渡</h3>
<p>用動畫填充短暫的等待時間。300ms 的 fade-in 動畫可以掩蓋 200ms 的 API 延遲，使用者感知到的是「動畫」而非「等待」。</p>
<h2 id="不要做的事">不要做的事</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>為什麼不好</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>每個操作都顯示全螢幕 loading</td>
          <td>阻斷所有互動，打斷心流</td>
      </tr>
      <tr>
          <td>spinner 顯示後立即消失（閃爍）</td>
          <td>視覺雜訊，比不顯示更差</td>
      </tr>
      <tr>
          <td>進度條從 0% 跳到 100%</td>
          <td>失去進度指示的意義</td>
      </tr>
      <tr>
          <td>假進度條（與實際進度無關）</td>
          <td>被識破後失去信任</td>
      </tr>
      <tr>
          <td>Loading 永不消失（無 timeout）</td>
          <td>介面凍結，使用者只能殺 app</td>
      </tr>
  </tbody>
</table>
<p>進度條類的反模式共享一個根因：進度指示的價值在於讓使用者校準預期，指示與實際脫鉤（假進度、0% 跳 100%）時信任一次性崩壞。拿不到真實進度時，用步驟計數（「3 / 5 個檔案」）或 indeterminate 動畫誠實表達「在跑、但無法預估」，優於編造百分比。</p>
<p>完成宣告的誠實度與進度指示同源。「完成」的判定條件若只是「連續 N 輪沒有變化」，分不出「真的到底了」和「載入通道根本沒打開」— 一個 Chrome extension 從 lazy-load 頁面提取書目，96/928 本就因 count 穩定誤判完成，使用者帶著殘缺資料離開且很難察覺（<a href="/blog/ux-design/cases/lazy-load-premature-completion/" data-link-title="U.C11 抓到 96/928 本就顯示完成 — 完成判定的證據強度不足" data-link-desc="批次 / 遍歷類操作宣告完成、實際只處理了一部分：「連續 N 輪沒有變化」是暫時停滯的訊號、不是窮盡的證據 — 完成判定需要獨立的窮盡證據（總數對照、終止標記）">U.C11</a>）。宣告完成要有窮盡證據（總數對照、終止標記、API 側的分頁 cursor 耗盡）；只有停滯訊號時，改為「已取得 N 筆、可能還有更多」的部分結果表達 — 下游需要全量的操作（備份、匯出）不適用降級、不完整按失敗處理。全螢幕 loading 的問題則是阻斷範圍 — 等待指示只需要覆蓋被等待的區域，不需要凍結整個介面。</p>
<h2 id="設計檢查清單">設計檢查清單</h2>
<ul>
<li><input disabled="" type="checkbox"> 操作的延遲分布（p90 / p95）是多少？對應哪個門檻帶？（跨帶判定另看 p50）</li>
<li><input disabled="" type="checkbox"> 低於 400ms 的操作：不顯示 spinner，用動畫過渡？</li>
<li><input disabled="" type="checkbox"> 400ms-1s 的操作：有按鈕 loading 狀態？</li>
<li><input disabled="" type="checkbox"> 超過 1s 的操作：有進度指示？</li>
<li><input disabled="" type="checkbox"> 超過 10s 的操作：有取消選項？</li>
<li><input disabled="" type="checkbox"> 有考慮感知效能技巧（骨架畫面 / 樂觀更新 / 漸進載入）？</li>
<li><input disabled="" type="checkbox"> spinner 有延遲顯示與最短顯示時間（防閃爍）？</li>
<li><input disabled="" type="checkbox"> 用 DevTools 網速節流（或 proxy 限速）模擬慢回應，驗證各門檻帶的 UI 都會出現？</li>
</ul>
<h2 id="參考來源">參考來源</h2>
<ul>
<li><strong>Doherty, W.J. &amp; Thadani, A.J.</strong> &ldquo;The Economic Value of Rapid Response Time.&rdquo; IBM, 1982（技術報告）— 400ms 門檻的原始研究</li>
<li><strong>Nielsen, J.</strong> Usability Engineering. Academic Press, 1993 — 100ms / 1s / 10s 三門檻模型</li>
<li><strong>Card, S.K., Moran, T.P. &amp; Newell, A.</strong> The Psychology of Human-Computer Interaction. Lawrence Erlbaum Associates, 1983 — 人機互動的認知心理學基礎</li>
<li><strong>Miller, R.B.</strong> &ldquo;Response Time in Man-Computer Conversational Transactions.&rdquo; Proceedings of the AFIPS Fall Joint Computer Conference, 1968 — 回應時間對使用者行為影響的早期研究</li>
<li><strong>Material Design 3: Loading Indicator</strong>（<a href="https://m3.material.io/components/loading-indicator">m3.material.io/components/loading-indicator</a>）— Google 的 loading 設計規範</li>
<li><strong>Laws of UX: Doherty Threshold</strong>（<a href="https://lawsofux.com/doherty-threshold">lawsofux.com/doherty-threshold</a>）— Doherty Threshold 的現代詮釋</li>
</ul>
]]></content:encoded></item><item><title>通知模式選擇：SnackBar、Dialog、Banner 與 Bottom Sheet</title><link>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/notification-pattern-selection/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/ux-design/06-interaction-feedback/notification-pattern-selection/</guid><description>&lt;h2 id="核心觀念">核心觀念&lt;/h2>
&lt;p>&lt;strong>通知的形式是一個設計決策，判準是干擾程度與使用者是否需要操作。&lt;/strong> 判準選錯，輕則通知被忽略（太低調），重則流程被打斷（太侵入）。&lt;a href="../feedback-three-layers/">三層回饋模型&lt;/a>把使用者操作後的回饋拆成點擊確認、等待指示、結果通知三層，本篇聚焦第三層（結果通知）的&lt;strong>形式選擇&lt;/strong> — 前篇回答「該通知什麼」，本篇回答「用什麼形式通知」。&lt;/p>
&lt;p>六種通知形式從低干擾到高干擾排成一道光譜：inline 狀態更新、SnackBar、Banner、Bottom Sheet、Dialog、全螢幕。每種形式對使用者的注意力佔用不同，選擇依據是兩個軸的交叉判讀。讀完本篇，你可以對每個操作結果選出干擾程度剛好的通知形式，避免「所有通知都用 Dialog」或「重要通知用 SnackBar 閃一下就消失」這兩類鏡像錯誤。&lt;/p>
&lt;h2 id="二軸判準">二軸判準&lt;/h2>
&lt;h3 id="軸一是否需要使用者操作">軸一：是否需要使用者操作&lt;/h3>
&lt;p>通知分「純告知」和「需要回應」兩類。&lt;/p>
&lt;p>&lt;strong>純告知&lt;/strong>：使用者只需要知道結果，不需要做任何事。「已複製到剪貼簿」「設定已儲存」「訊息已送出」。這類通知可以自動消失 — 使用者瞄一眼就夠了，停留太久反而擋視線。&lt;/p>
&lt;p>&lt;strong>需要回應&lt;/strong>：使用者必須做出選擇或確認才能繼續。「確定要刪除嗎？」「匯出完成，檔案在這裡」「批次操作有 3 筆失敗」。這類通知必須持續顯示直到使用者處理，自動消失等於系統擅自幫使用者做了「忽略」這個決定。&lt;/p>
&lt;p>邊界情境：帶「復原」選項的刪除通知。使用者不操作也行（刪除生效），操作了也行（復原）。這屬於「可選操作」— SnackBar 加一顆 action button 是標準解法，給使用者反悔窗口但不強制回應。&lt;/p>
&lt;h3 id="軸二干擾程度">軸二：干擾程度&lt;/h3>
&lt;p>干擾程度由兩個因素決定：通知是否遮擋當前內容（視覺佔用），以及使用者能否繼續操作其他功能（互動阻斷）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>等級&lt;/th>
 &lt;th>視覺佔用&lt;/th>
 &lt;th>互動阻斷&lt;/th>
 &lt;th>代表形式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>零&lt;/td>
 &lt;td>不額外佔用空間&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>Inline 狀態更新&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>小面積 overlay&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>SnackBar&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中&lt;/td>
 &lt;td>局部固定區域&lt;/td>
 &lt;td>否&lt;/td>
 &lt;td>Banner&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中高&lt;/td>
 &lt;td>螢幕下半&lt;/td>
 &lt;td>部分&lt;/td>
 &lt;td>Bottom Sheet（modal）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>居中 overlay&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>Dialog&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>最高&lt;/td>
 &lt;td>整個螢幕&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>全螢幕 / 新畫面&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>干擾程度的選擇原則是「剛好夠用」— 確認已複製到剪貼簿不需要 Dialog，刪除確認不能用 SnackBar。&lt;/p>
&lt;h2 id="通知形式光譜">通知形式光譜&lt;/h2>
&lt;h3 id="inline-狀態更新">Inline 狀態更新&lt;/h3>
&lt;p>UI 元素在原位更新，不彈出額外元件。使用者按了按鈕，按鈕本身或旁邊的區域就反映結果。&lt;/p>
&lt;p>收藏按鈕填滿、列表項目滑動移除、表單欄位驗證紅字、數量即時更新 — 結果就在使用者視線所在位置，不需要另開通知通道。&lt;a href="../feedback-three-layers/">三層回饋模型&lt;/a>第一層（點擊確認）和第三層（結果通知）在同一個 UI 元素上完成時，inline 是唯一合理的形式。&lt;/p>
&lt;p>適用條件：操作結果在觸發元素的 UI context 內就能完整呈現。不適合需要額外資訊（檔案路徑、失敗原因）或離操作位置較遠的結果。&lt;/p>
&lt;h3 id="snackbar">SnackBar&lt;/h3>
&lt;p>螢幕底部的短暫訊息條，可選配一顆 action button。出現後自動消失，使用者也可以手動滑掉。同一時間只顯示一個 — 新的 SnackBar 會取代舊的。&lt;/p>
&lt;p>Material Design 的 SnackBar 是 Android 平台 Toast 的設計升級 — Toast 只能顯示文字且不可互動，SnackBar 加入了 action button（最多一個）和 swipe-to-dismiss。iOS 沒有官方對等元件，多數跨平台 app 統一使用 SnackBar 行為。&lt;/p>
&lt;p>&lt;strong>適用條件&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>操作已完成，使用者不需要做進一步決定。「已儲存」「已複製」「已加入購物車」&lt;/li>
&lt;li>提供可選但不強制的操作。「已刪除」＋「復原」button — 不點也沒關係，刪除照常生效&lt;/li>
&lt;li>結果資訊量低，一句話能講完的確認訊息&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>不適合&lt;/strong>：需要使用者確認才能繼續的決策（用 Dialog）、資訊量大到一句話裝不下的結果（用 Dialog 或 Bottom Sheet）、持續性狀態變更通知（用 Banner）。&lt;/p>
&lt;h3 id="banner">Banner&lt;/h3>
&lt;p>SnackBar 消失後使用者就忘記自己在離線中，30 秒後送出表單才撞上網路錯誤 — 這類持續性狀態需要一種不會自動消失的通知。Banner 固定在內容區頂部（app bar 下方），保持顯示直到使用者主動 dismiss 或觸發條件消失，可以有一到兩顆 action button。&lt;/p>
&lt;p>Banner 和 SnackBar 的關鍵差異是&lt;strong>生命週期&lt;/strong>：SnackBar 是事件驅動（「這件事剛發生」），Banner 是狀態驅動（「這個條件持續存在」）。「已恢復連線」是事件、用 SnackBar；「目前離線，部分功能不可用」是持續狀態、用 Banner。&lt;a href="https://tarrragon.github.io/blog/ux-design/04-error-recovery/degraded-mode-design/" data-link-title="Degraded mode 設計" data-link-desc="部分功能不可用時怎麼告知使用者 — 靜默隱藏 vs 明確標示 vs 替代方案的設計取捨">Degraded mode&lt;/a>（系統部分功能因外部依賴不可用而暫時無法運作）的進入退出是這個差異的典型場景 — 進入降級是持續狀態，用 Banner 比 SnackBar 合適，因為 SnackBar 消失後使用者會忘記自己在降級中；退出降級是一次性事件，用 SnackBar。&lt;/p>
&lt;p>典型使用場景：持續性狀態需要使用者知曉（「離線模式」「版本過舊，部分功能受限」）、非緊急但需要使用者在某個時間點處理（「有新版本可更新」＋「立即更新」「稍後」）、影響整個畫面的使用者操作（「篩選已啟用，顯示的是部分結果」）。一次性事件通知（用 SnackBar）和需要立即決策的重要操作（用 Dialog）不適合 Banner。&lt;/p>
&lt;h3 id="bottom-sheet">Bottom Sheet&lt;/h3>
&lt;p>Dialog 佔用焦點但面積有限，需要更大空間展開選項又不想離開當前畫面時，Bottom Sheet 從螢幕底部滑出、覆蓋部分內容。Modal bottom sheet 有半透明遮罩，使用者必須選擇或 dismiss 才能繼續操作下方內容；non-modal bottom sheet 與下方內容並存，使用者可以在不 dismiss 的情況下切換注意力。&lt;/p>
&lt;p>分享選單、篩選面板、地圖上的地點詳情、設定快捷面板 — 共通點是操作與當前畫面高度相關，全螢幕導航會破壞 context。列表某一項的詳細資訊、日期選擇器這類需要一定面積但不值得開新畫面的互動也適合用 Bottom Sheet。&lt;/p>
&lt;p>iOS 的 Action Sheet 是 Bottom Sheet 的特化形式，專門呈現「針對當前操作的選項列表」（分享、更多操作）。iOS 13+ 的 sheet presentation 把 Bottom Sheet 擴展為通用 UI 容器（可拖曳到不同高度、可滑動 dismiss）。兩個平台的共識是：Bottom Sheet 用在「延伸當前操作」，Dialog 用在「要求使用者做決定」。簡單的操作確認（用 SnackBar 或 Dialog 足夠）和重要決策確認（Dialog 的阻斷性是設計需求，Bottom Sheet 太容易被意外滑掉 dismiss）不適合 Bottom Sheet。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心觀念">核心觀念</h2>
<p><strong>通知的形式是一個設計決策，判準是干擾程度與使用者是否需要操作。</strong> 判準選錯，輕則通知被忽略（太低調），重則流程被打斷（太侵入）。<a href="../feedback-three-layers/">三層回饋模型</a>把使用者操作後的回饋拆成點擊確認、等待指示、結果通知三層，本篇聚焦第三層（結果通知）的<strong>形式選擇</strong> — 前篇回答「該通知什麼」，本篇回答「用什麼形式通知」。</p>
<p>六種通知形式從低干擾到高干擾排成一道光譜：inline 狀態更新、SnackBar、Banner、Bottom Sheet、Dialog、全螢幕。每種形式對使用者的注意力佔用不同，選擇依據是兩個軸的交叉判讀。讀完本篇，你可以對每個操作結果選出干擾程度剛好的通知形式，避免「所有通知都用 Dialog」或「重要通知用 SnackBar 閃一下就消失」這兩類鏡像錯誤。</p>
<h2 id="二軸判準">二軸判準</h2>
<h3 id="軸一是否需要使用者操作">軸一：是否需要使用者操作</h3>
<p>通知分「純告知」和「需要回應」兩類。</p>
<p><strong>純告知</strong>：使用者只需要知道結果，不需要做任何事。「已複製到剪貼簿」「設定已儲存」「訊息已送出」。這類通知可以自動消失 — 使用者瞄一眼就夠了，停留太久反而擋視線。</p>
<p><strong>需要回應</strong>：使用者必須做出選擇或確認才能繼續。「確定要刪除嗎？」「匯出完成，檔案在這裡」「批次操作有 3 筆失敗」。這類通知必須持續顯示直到使用者處理，自動消失等於系統擅自幫使用者做了「忽略」這個決定。</p>
<p>邊界情境：帶「復原」選項的刪除通知。使用者不操作也行（刪除生效），操作了也行（復原）。這屬於「可選操作」— SnackBar 加一顆 action button 是標準解法，給使用者反悔窗口但不強制回應。</p>
<h3 id="軸二干擾程度">軸二：干擾程度</h3>
<p>干擾程度由兩個因素決定：通知是否遮擋當前內容（視覺佔用），以及使用者能否繼續操作其他功能（互動阻斷）。</p>
<table>
  <thead>
      <tr>
          <th>等級</th>
          <th>視覺佔用</th>
          <th>互動阻斷</th>
          <th>代表形式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>零</td>
          <td>不額外佔用空間</td>
          <td>否</td>
          <td>Inline 狀態更新</td>
      </tr>
      <tr>
          <td>低</td>
          <td>小面積 overlay</td>
          <td>否</td>
          <td>SnackBar</td>
      </tr>
      <tr>
          <td>中</td>
          <td>局部固定區域</td>
          <td>否</td>
          <td>Banner</td>
      </tr>
      <tr>
          <td>中高</td>
          <td>螢幕下半</td>
          <td>部分</td>
          <td>Bottom Sheet（modal）</td>
      </tr>
      <tr>
          <td>高</td>
          <td>居中 overlay</td>
          <td>是</td>
          <td>Dialog</td>
      </tr>
      <tr>
          <td>最高</td>
          <td>整個螢幕</td>
          <td>是</td>
          <td>全螢幕 / 新畫面</td>
      </tr>
  </tbody>
</table>
<p>干擾程度的選擇原則是「剛好夠用」— 確認已複製到剪貼簿不需要 Dialog，刪除確認不能用 SnackBar。</p>
<h2 id="通知形式光譜">通知形式光譜</h2>
<h3 id="inline-狀態更新">Inline 狀態更新</h3>
<p>UI 元素在原位更新，不彈出額外元件。使用者按了按鈕，按鈕本身或旁邊的區域就反映結果。</p>
<p>收藏按鈕填滿、列表項目滑動移除、表單欄位驗證紅字、數量即時更新 — 結果就在使用者視線所在位置，不需要另開通知通道。<a href="../feedback-three-layers/">三層回饋模型</a>第一層（點擊確認）和第三層（結果通知）在同一個 UI 元素上完成時，inline 是唯一合理的形式。</p>
<p>適用條件：操作結果在觸發元素的 UI context 內就能完整呈現。不適合需要額外資訊（檔案路徑、失敗原因）或離操作位置較遠的結果。</p>
<h3 id="snackbar">SnackBar</h3>
<p>螢幕底部的短暫訊息條，可選配一顆 action button。出現後自動消失，使用者也可以手動滑掉。同一時間只顯示一個 — 新的 SnackBar 會取代舊的。</p>
<p>Material Design 的 SnackBar 是 Android 平台 Toast 的設計升級 — Toast 只能顯示文字且不可互動，SnackBar 加入了 action button（最多一個）和 swipe-to-dismiss。iOS 沒有官方對等元件，多數跨平台 app 統一使用 SnackBar 行為。</p>
<p><strong>適用條件</strong>：</p>
<ul>
<li>操作已完成，使用者不需要做進一步決定。「已儲存」「已複製」「已加入購物車」</li>
<li>提供可選但不強制的操作。「已刪除」＋「復原」button — 不點也沒關係，刪除照常生效</li>
<li>結果資訊量低，一句話能講完的確認訊息</li>
</ul>
<p><strong>不適合</strong>：需要使用者確認才能繼續的決策（用 Dialog）、資訊量大到一句話裝不下的結果（用 Dialog 或 Bottom Sheet）、持續性狀態變更通知（用 Banner）。</p>
<h3 id="banner">Banner</h3>
<p>SnackBar 消失後使用者就忘記自己在離線中，30 秒後送出表單才撞上網路錯誤 — 這類持續性狀態需要一種不會自動消失的通知。Banner 固定在內容區頂部（app bar 下方），保持顯示直到使用者主動 dismiss 或觸發條件消失，可以有一到兩顆 action button。</p>
<p>Banner 和 SnackBar 的關鍵差異是<strong>生命週期</strong>：SnackBar 是事件驅動（「這件事剛發生」），Banner 是狀態驅動（「這個條件持續存在」）。「已恢復連線」是事件、用 SnackBar；「目前離線，部分功能不可用」是持續狀態、用 Banner。<a href="/blog/ux-design/04-error-recovery/degraded-mode-design/" data-link-title="Degraded mode 設計" data-link-desc="部分功能不可用時怎麼告知使用者 — 靜默隱藏 vs 明確標示 vs 替代方案的設計取捨">Degraded mode</a>（系統部分功能因外部依賴不可用而暫時無法運作）的進入退出是這個差異的典型場景 — 進入降級是持續狀態，用 Banner 比 SnackBar 合適，因為 SnackBar 消失後使用者會忘記自己在降級中；退出降級是一次性事件，用 SnackBar。</p>
<p>典型使用場景：持續性狀態需要使用者知曉（「離線模式」「版本過舊，部分功能受限」）、非緊急但需要使用者在某個時間點處理（「有新版本可更新」＋「立即更新」「稍後」）、影響整個畫面的使用者操作（「篩選已啟用，顯示的是部分結果」）。一次性事件通知（用 SnackBar）和需要立即決策的重要操作（用 Dialog）不適合 Banner。</p>
<h3 id="bottom-sheet">Bottom Sheet</h3>
<p>Dialog 佔用焦點但面積有限，需要更大空間展開選項又不想離開當前畫面時，Bottom Sheet 從螢幕底部滑出、覆蓋部分內容。Modal bottom sheet 有半透明遮罩，使用者必須選擇或 dismiss 才能繼續操作下方內容；non-modal bottom sheet 與下方內容並存，使用者可以在不 dismiss 的情況下切換注意力。</p>
<p>分享選單、篩選面板、地圖上的地點詳情、設定快捷面板 — 共通點是操作與當前畫面高度相關，全螢幕導航會破壞 context。列表某一項的詳細資訊、日期選擇器這類需要一定面積但不值得開新畫面的互動也適合用 Bottom Sheet。</p>
<p>iOS 的 Action Sheet 是 Bottom Sheet 的特化形式，專門呈現「針對當前操作的選項列表」（分享、更多操作）。iOS 13+ 的 sheet presentation 把 Bottom Sheet 擴展為通用 UI 容器（可拖曳到不同高度、可滑動 dismiss）。兩個平台的共識是：Bottom Sheet 用在「延伸當前操作」，Dialog 用在「要求使用者做決定」。簡單的操作確認（用 SnackBar 或 Dialog 足夠）和重要決策確認（Dialog 的阻斷性是設計需求，Bottom Sheet 太容易被意外滑掉 dismiss）不適合 Bottom Sheet。</p>
<h3 id="dialog">Dialog</h3>
<p>有些操作的後果大到使用者必須被強制暫停 — 刪除不可逆、匯出結果包含使用者必須知道的檔案路徑、付款金額需要最後確認。Dialog 是居中的 modal overlay，半透明遮罩阻斷背景互動，使用者必須回應（選擇按鈕或 dismiss）才能繼續。它同時佔用視覺焦點和阻斷所有其他操作，是最強的注意力攫取手段之一。</p>
<p>三種常見型態：</p>
<p><strong>Alert Dialog</strong>：告知重要結果或狀態，使用者確認後繼續。例如匯出完成後顯示檔案路徑的 Alert Dialog — 使用者需要看到路徑才能找到匯出結果（完整案例見 <a href="/blog/ux-design/cases/export-button-zero-feedback/" data-link-title="U.C5 匯出按鈕按下零回饋 — 狀態機完備但 UI 沒接線" data-link-desc="Flutter app 匯出設定頁的確認匯出按鈕 onPressed 是空 callback，按下畫面毫無變化 — 使用者無法分辨匯出成功、進行中、還是功能根本沒做。ViewModel 的 idle/inProgress/completed/failed 狀態機早已完備，缺的只是頁面接線與三層回饋">匯出按鈕零回饋</a>）。</p>
<p><strong>Confirmation Dialog</strong>：使用者做出會產生後果的決定前確認。「確定刪除這 3 個項目？此操作無法復原」＋「刪除」「取消」。操作不可逆或代價高時，Dialog 的阻斷性是設計需求 — 讓使用者在執行前有一個強制暫停點。</p>
<p><strong>Selection Dialog</strong>：從多個選項中做一次性選擇。從列表中選一個類別、從月曆中選日期。Material Design 建議選項超過兩三個時考慮用 Bottom Sheet 或全螢幕，因為 Dialog 的面積有限。</p>
<p>Dialog 的保護效果取決於使用頻率。操作不可逆或代價高、操作結果包含關鍵資訊、必須在當前流程中做二選一決定 — 這三類情境是 Dialog 的正確使用場景。但輕量確認（「已儲存」）用 Dialog 是殺雞用牛刀、SnackBar 足夠。頻繁操作每次都彈 Dialog 等於每次都打斷流程，使用者訓練出的反應是「不讀直接按確定」，真正重要的確認也會被跳過。大量內容或複雜互動會超出 Dialog 的有限面積，改用 Bottom Sheet 或全螢幕。</p>
<p><strong>Dialog vs Bottom Sheet 的決策邊界</strong>：延伸當前操作的選項（分享、篩選、更多操作）用 Bottom Sheet，要求使用者做二選一決定（刪除確認、付款確認）用 Dialog。兩者重疊的地帶是 Selection Dialog — 選項少（二到三個）時 Dialog 的居中面積夠用，選項多時 Material Design 建議升級到 Bottom Sheet 或全螢幕。區分它們的是行為屬性：Dialog 是 <a href="/blog/ux-design/knowledge-cards/modal/" data-link-title="Modal（模態）" data-link-desc="UI 元素阻斷背景互動、要求使用者回應後才能繼續 — 區分 Dialog 與 non-modal Bottom Sheet 的關鍵行為屬性">modal</a>（阻斷背景互動、強制回應），Bottom Sheet 可以是 modal 也可以是 non-modal，且 Bottom Sheet 太容易被滑掉 dismiss，承載重要決策時使用者可能意外關閉。</p>
<h3 id="全螢幕">全螢幕</h3>
<p>整個畫面被替換為通知或結果內容。在 Material Design 中稱為 full-screen dialog，在 navigation 架構中就是 push 一個新畫面。</p>
<p>批次操作中部分項目成功、部分失敗時（<a href="../feedback-three-layers/">三層回饋模型</a>稱為「部分成功」），摘要加明細的資訊密度通常高到只有全螢幕才放得下。嚴重錯誤需要完整展示標題、原因說明與行動指引（<a href="/blog/ux-design/04-error-recovery/error-message-principles/" data-link-title="錯誤訊息撰寫原則" data-link-desc="錯誤訊息的兩個職責：使用者能讀懂發生什麼、使用者能決定下一步做什麼">錯誤訊息撰寫原則</a>的三層結構），這種資訊密度同樣需要全螢幕作為容器。</p>
<p><strong>適用條件</strong>：</p>
<ul>
<li>結果內容需要完整閱讀和互動。批次操作的詳細結果（87 成功 / 13 失敗 + 每筆失敗的原因與修正操作）</li>
<li>流程的終結畫面。付款完成確認頁（金額、訂單編號、預計送達）</li>
<li>需要使用者在結果上做進一步操作。匯入結果的逐筆確認修正</li>
</ul>
<h2 id="選擇矩陣">選擇矩陣</h2>
<p>本篇聚焦使用者操作後的結果通知。系統主動發起的通知（背景任務完成、伺服器推播、排程提醒）的形式選擇涉及 OS 層通知中心與 app 內通知的分工，不在本篇範圍。</p>
<p>把二軸交叉後，每個格子對應一到兩種適合的通知形式。矩陣同時涵蓋操作後的結果通知和操作前的預防性確認（第六列），因為兩者都需要在同一組通知形式中做選擇：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>需要使用者操作？</th>
          <th>干擾程度建議</th>
          <th>推薦形式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>操作成功，結果已反映在 UI</td>
          <td>否</td>
          <td>零</td>
          <td>Inline 狀態更新</td>
      </tr>
      <tr>
          <td>操作成功，結果不在視線範圍</td>
          <td>否</td>
          <td>低</td>
          <td>SnackBar</td>
      </tr>
      <tr>
          <td>操作成功，帶可選的復原</td>
          <td>可選</td>
          <td>低</td>
          <td>SnackBar + action</td>
      </tr>
      <tr>
          <td>持續性狀態變更</td>
          <td>否/可選</td>
          <td>中</td>
          <td>Banner</td>
      </tr>
      <tr>
          <td>操作結果需要選後續操作</td>
          <td>是</td>
          <td>中高</td>
          <td>Bottom Sheet</td>
      </tr>
      <tr>
          <td>操作不可逆，需確認（操作前）</td>
          <td>是</td>
          <td>高</td>
          <td>Dialog</td>
      </tr>
      <tr>
          <td>操作結果含關鍵資訊</td>
          <td>是</td>
          <td>高</td>
          <td>Dialog</td>
      </tr>
      <tr>
          <td>操作結果內容密度高，需要閱讀和進一步操作</td>
          <td>是</td>
          <td>最高</td>
          <td>全螢幕</td>
      </tr>
  </tbody>
</table>
<h3 id="補充判準操作頻率">補充判準：操作頻率</h3>
<p>矩陣是判讀起點，但二軸無法區分「刪除帳號」和「刪除待辦事項」— 兩者在軸一（需要操作：是）和軸二（干擾程度：高）的讀數相同。區分它們的是操作頻率：低頻高代價的刪除（刪帳號）用 Dialog 確認，高頻低代價的刪除（刪待辦事項）用 SnackBar + 復原。頻率高的操作應該降級通知形式 — 每次加入購物車都彈 Dialog 會讓 Dialog 的保護效果在第三次點擊時歸零。</p>
<h2 id="自動消失的時間設計">自動消失的時間設計</h2>
<p>SnackBar 的自動消失時間取決於使用者閱讀內容和決定是否操作所需的時間。</p>
<table>
  <thead>
      <tr>
          <th>內容類型</th>
          <th>建議停留時間</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>純文字確認（無 action）</td>
          <td>4 秒</td>
          <td>一句話的閱讀時間，足夠瞄一眼（Material Design 短時長慣例值）</td>
      </tr>
      <tr>
          <td>帶 action button</td>
          <td>8-10 秒</td>
          <td>使用者需要讀訊息 + 決定是否按 action（Material Design 範圍上端的推導值）</td>
      </tr>
  </tbody>
</table>
<p>Material Design 建議 SnackBar 顯示 4-10 秒。帶 action 時取範圍上端（8-10 秒）是基於「使用者需要讀完訊息再決定是否操作」的推導，MD 規範本身未針對帶 action 的情境單獨指定子範圍。下限低於 4 秒的風險是使用者來不及讀完（螢幕閱讀器使用者尤其受影響），上限超過 10 秒則開始干擾後續操作。</p>
<p><strong>無障礙考量</strong>：帶 action 的 SnackBar 若自動消失且 action 不可復得，對依賴螢幕閱讀器的使用者是可用性問題（WCAG 2.1 SC 2.2.1 要求自動消失的內容可延長或可暫停）。替代策略：action 消失後仍可在別處操作（如復原也能從歷史記錄觸發），或螢幕閱讀器啟用時延長停留時間。</p>
<p>Banner 和 Dialog 不自動消失 — 它們承載的資訊或決策不能被時間限制替代。Banner 在觸發條件消失時自動移除（「離線模式」在恢復連線後消失）；Dialog 在使用者回應後關閉。</p>
<h2 id="堆疊與排程">堆疊與排程</h2>
<p>多個通知同時到達或短時間連續到達時，形式本身的堆疊規則決定使用者體驗。</p>
<p>SnackBar、Dialog、Bottom Sheet 都遵循「同一時間只顯示一個」的規則，但衝突時的行為不同。SnackBar 新舊替換 — 新的到達時當前的立即 dismiss；快速連續觸發（批次操作逐筆回饋）只有最後一條存活，這種場景應彙整為一條摘要（「已匯入 12 筆」）或改用全螢幕明細。Dialog 之上再彈 Dialog 是嚴重的 UX 問題 — 使用者的注意力不知道該放哪裡，dismiss 順序混亂；業務流程確實需要連續確認時，改為多步驟的單一 Dialog 或全螢幕流程。</p>
<p>Banner 是例外 — 多條 Banner 垂直堆疊，而非互相替換。超過兩條時畫面可用空間嚴重縮減；如果同時存在多個持續性狀態（離線 + 版本過舊 + 同步衝突），考慮合併為一條摘要 Banner 或改用全域狀態列。</p>
<p>少數可接受的跨形式堆疊：Bottom Sheet 之上彈 Dialog。Bottom Sheet 提供操作 context，Dialog 確認該操作 — 兩者的語意分工清楚，使用者不會混淆。</p>
<h2 id="反模式">反模式</h2>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>症狀</th>
          <th>修正</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>所有通知都用 Dialog</td>
          <td>使用者訓練出「不讀直接按確定」，真正重要的確認被跳過</td>
          <td>依二軸判準降級：純告知用 SnackBar，持續狀態用 Banner</td>
      </tr>
      <tr>
          <td>需要使用者操作的通知用 SnackBar</td>
          <td>SnackBar 自動消失，使用者還沒讀完就不見了</td>
          <td>改用 Dialog 或延長停留時間 + 保留 action 在別處</td>
      </tr>
      <tr>
          <td>頻繁操作的每次結果都彈 Dialog</td>
          <td>流程不斷被打斷，操作效率下降</td>
          <td>改用 SnackBar 或 inline 狀態更新</td>
      </tr>
      <tr>
          <td>離線狀態用 SnackBar 通知</td>
          <td>SnackBar 消失後使用者忘記自己在離線，操作失敗時感到困惑</td>
          <td>改用 Banner，持續顯示直到恢復連線</td>
      </tr>
      <tr>
          <td>Dialog 裡再彈 Dialog</td>
          <td>使用者注意力分裂，dismiss 順序混亂</td>
          <td>合併為一個 Dialog 或改為多步驟流程</td>
      </tr>
      <tr>
          <td>SnackBar 堆疊成串（逐筆回饋批次操作）</td>
          <td>只看到最後一條，前面的都被蓋掉</td>
          <td>彙整為一條摘要或改用全螢幕明細</td>
      </tr>
  </tbody>
</table>
<p>第一條和第二條是最常見的一對鏡像錯誤 — 前者過度使用 Dialog 稀釋了它的保護效果，後者讓需要操作的通知被時間自動沖掉。兩者的根因相同：沒有用「是否需要使用者操作」這個軸做區分。</p>
<p>第三條（頻繁 Dialog）的典型場景：電商 app 每次加入購物車都彈確認 Dialog，使用者連續加五件商品要按五次「確定」— 第三次開始就不看內容直接按，到真正需要確認的結帳 Dialog 也沿用同一反射動作。第四條（離線 SnackBar）：使用者在地鐵中斷線，SnackBar 閃一下「已離線」然後消失，30 秒後使用者填完表單送出，得到的是毫無脈絡的網路錯誤。第五條（Dialog 堆疊）：刪除按鈕彈確認 Dialog，確認後觸發權限檢查又彈第二個 Dialog，使用者按錯層 dismiss 會取消真正想做的操作。第六條（SnackBar 成串）：批次匯入 50 筆資料，每筆成功都彈 SnackBar，50 條在 4 秒內輪替，使用者只看到最後一條「第 50 筆匯入成功」，不知道前 49 筆的狀態。</p>
<h2 id="設計檢查清單">設計檢查清單</h2>
<p>為每個操作的結果通知逐一確認：</p>
<ul>
<li><input disabled="" type="checkbox"> 這個通知需要使用者操作嗎？（需要 → 不能自動消失）</li>
<li><input disabled="" type="checkbox"> 干擾程度是否匹配通知的重要性？（不可逆操作 → Dialog，常規確認 → SnackBar）</li>
<li><input disabled="" type="checkbox"> 自動消失的時間足夠使用者讀完內容並操作嗎？</li>
<li><input disabled="" type="checkbox"> 多個通知同時到達時的堆疊行為是否合理？</li>
<li><input disabled="" type="checkbox"> 持續性狀態用 Banner 而非 SnackBar？</li>
<li><input disabled="" type="checkbox"> 高頻操作的通知夠低調嗎？（不會每次都打斷流程）</li>
<li><input disabled="" type="checkbox"> 批次操作的結果有彙整，而非逐筆彈 SnackBar？</li>
</ul>
<h2 id="參考來源">參考來源</h2>
<ul>
<li><strong>Material Design 3: Snackbar</strong>（<a href="https://m3.material.io/components/snackbar">m3.material.io/components/snackbar</a>）— SnackBar 的設計規範與使用時機</li>
<li><strong>Material Design 3: Dialogs</strong>（<a href="https://m3.material.io/components/dialogs">m3.material.io/components/dialogs</a>）— Dialog 的三種型態與使用準則</li>
<li><strong>Material Design 3: Bottom Sheets</strong>（<a href="https://m3.material.io/components/bottom-sheets">m3.material.io/components/bottom-sheets</a>）— Bottom Sheet 的 modal 與 non-modal 使用場景</li>
<li><strong>Apple Human Interface Guidelines: Alerts</strong>（Apple Developer Documentation）— iOS Alert 的使用時機：需要使用者注意的重要資訊</li>
<li><strong>Apple Human Interface Guidelines: Action Sheets</strong>（Apple Developer Documentation）— iOS Action Sheet 的設計規範</li>
<li><strong>Nielsen Norman Group: &ldquo;Modal &amp; Nonmodal Dialogs&rdquo;</strong>（nngroup.com, 2017）— Modal 與 nonmodal 的使用時機研究</li>
</ul>
]]></content:encoded></item></channel></rss>