<?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>Usability on Tarragon</title><link>https://tarrragon.github.io/blog/tags/usability/</link><description>Recent content in Usability 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/tags/usability/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></channel></rss>