核心觀念

介面設計中,使用者每執行一個動作,系統都必須提供回饋。回饋是可用性的基本要求。沒有回饋的按鈕,使用者無法區分「系統在處理」和「按鈕壞了」。

這個原則對應 Jakob Nielsen 十大可用性啟發法的第一條:系統狀態可見性(Visibility of System Status) — 系統應透過適當的回饋,在合理時間內告知使用者目前發生了什麼。

三層回饋模型

使用者按下按鈕後,回饋依時間順序分三層:

第一層:點擊確認(0-100ms)

系統確認「收到你的操作了」。

回饋形式範例適用場景
視覺狀態變化按鈕顏色變深、凹陷效果所有按鈕
水波紋效果Material Design ripple觸控介面
觸覺回饋震動行動裝置
聲音回饋輕微點擊聲無障礙輔助

設計原則:第一層回饋必須在 100ms 內出現。這個門檻來自人類感知研究 — 100ms 以內的延遲被感知為即時反應,超過後點擊與回饋之間開始出現可察覺的落差,回饋就失去「確認」的效果。平台原生元件的 pressed / ripple 互動態已滿足這個門檻,用平台預設互動態即自動合格;自建元件(自繪按鈕、canvas UI)才需要自行驗證回饋延遲。

第二層:等待指示(100ms - 10s)

系統告知「正在處理你的請求」。

等待時長建議回饋說明
< 400ms不需額外指示Doherty Threshold:400ms 內完成、人機互動不需互相等待
400ms - 1sSpinner / 按鈕 loading 狀態簡單指示「還在跑」
1s - 10s進度條或步驟指示告知使用者「到哪了」
> 10s進度百分比 + 預估剩餘時間超過 10s 使用者開始考慮放棄

設計原則:等待期間必須禁用觸發按鈕(disabled 狀態),防止使用者因焦慮而重複點擊導致重複提交。UI 層的防重複提交降低發生率;最終防線是伺服器端的冪等設計 — 重複請求真的到達後端時要能被識別與去重。兩層都要做,本系列只涵蓋 UI 層。

400ms 門檻(Doherty Threshold 的實務意義:非同步操作通常在 400ms 內完成時,可省略 loading 指示器、直接跳到第三層的結果回饋(點擊確認仍要在第一層提供)。這條門檻的出身與適用邊界、以及實際延遲有快有慢時怎麼對表,時間感知與回應策略有完整推導。

第三層:結果通知

系統告知「操作的結果是什麼」。

結果回饋形式設計要求
成功畫面更新 / 成功提示 / 跳轉讓使用者看到操作造成的改變
失敗錯誤訊息 + 可執行動作告知原因 + 提供下一步(重試 / 替代方案)
空結果空狀態畫面區別「沒找到」和「沒載入」
部分成功摘要 + 明細告知哪些成功哪些失敗

設計原則:結果通知必須恢復按鈕到可操作狀態。操作結束後按鈕仍顯示 loading = 介面凍結。

空結果的呈現:空狀態畫面要讓使用者分得出「查詢成功但沒有資料」和「還沒載入完」— 給明確文字(「沒有符合的結果」)與下一步建議(放寬條件、建立第一筆資料),載入中則維持等待指示。空狀態與錯誤狀態的措辭由模組四:錯誤訊息撰寫原則接手。

部分成功的呈現:批次操作(匯入 100 筆、87 筆成功)用摘要加明細兩層:摘要先告知整體結果(「87 筆成功、13 筆失敗」),明細列出失敗項與各自的修正動作,讓使用者只需處理失敗的部分、而非重跑整批。

結果通知的鏈路前提:第三層的設計都假設「結果會正確到達 UI」。UI 與執行端分屬不同 context 時(browser extension 的 popup / content script / service worker、跨 process 架構),結果要跨訊息通道才到 UI — 通道語意錯誤或事件訂閱缺失會讓通知消失、甚至反轉。一個 Chrome extension 提取成功 96 本且已寫入 storage,popup 卻顯示「提取失敗」— async listener 把 undefined 搶先當回應送回(U.C9)。跨 context 的結果通知要有端對端驗證(操作成功 → UI 顯示成功),不只測 UI 收到資料後的呈現。

本篇聚焦「該通知什麼」;SnackBar / Dialog / Banner / Bottom Sheet 之間的形式選擇由通知模式選擇展開。螢幕閱讀器的通知宣告(aria-live / aria-busy)屬無障礙實作,不在本模組範圍。

兩類按鈕的回饋設計

依操作的時間特性,按鈕分兩類(各狀態的視覺設計在按鈕狀態設計展開):

非同步按鈕(API / 資料庫 / 權限請求)

完整三層回饋:

1[idle] → 點擊 → [loading: disabled + spinner] → 完成 → [idle + 結果通知]

設計要點:

  • loading 期間按鈕 disabled(防重複提交)
  • 按鈕文字可變(「送出」→「送出中…」)
  • 結束後恢復 idle 狀態

同步按鈕(導航 / 切換 / 開關)

僅需第一層 + 防連點:

1[idle] → 點擊 → [視覺回饋 + 執行] → [完成]

設計要點:

  • 操作在 100ms 內完成,不需 loading
  • 加入防連點(300ms 上下的慣例值)避免快速連點觸發多次導航
  • 視覺回饋(ripple / 狀態切換)即足夠

防連點的語意:第一次點擊立即執行、之後 300ms 內的點擊忽略(leading-edge debounce),或用導航鎖(in-flight flag)在導航完成前拒絕新請求。反過來「等 300ms 沒有新點擊才執行」(trailing debounce)會給每次導航加上 300ms 延遲,直接違反第一層的 100ms 門檻 — 防連點防的是第二次點擊,第一次要立即生效。

畫面級回饋:多步驟流程的狀態轉換

上述按鈕級回饋處理的是「按一個按鈕」的單一操作。但有些使用者流程涉及整個畫面的狀態轉換 — 連線、配對、同步、多步驟表單 — 每個中間狀態都需要能被使用者區辨的 UI 回饋。

與按鈕級的差異

維度按鈕級回饋畫面級回饋
範圍單一按鈕內的狀態變化整個畫面 UI 替換
狀態數2-3 個(idle / loading / result)4-6 個(idle / processing / success / error / disconnected…)
退出路徑不需要(按鈕自己恢復)每個中間狀態都必須有退出路徑
設計工具按鈕狀態檢查清單畫面狀態矩陣(模組一)

設計模式

以連線流程為例:

1[idle] ─點擊連線→ [connecting] ─成功→ [connected]
2                       │                    │
3                       ├─失敗→ [error]       ├─斷線→ [disconnected]
4                       └─取消→ [idle]        └─手動斷開→ [idle]

每個狀態的回饋設計:

狀態UI 呈現可用操作退出路徑
idle連線按鈕點擊連線返回上一頁
connecting全頁 spinner + 「連線中」文字取消取消 → idle
connected功能畫面使用功能 / 斷開斷開 → idle / 返回
error錯誤訊息 + 重連按鈕重連 / 返回重連 → connecting / 返回 → idle
disconnected「連線中斷」+ 重連按鈕重連 / 返回重連 → connecting / 返回 → idle

關鍵設計原則:error 和 disconnected 必須同時提供「重連」和「返回」兩條路徑。只有重連會把使用者鎖在錯誤迴圈(如果問題無法透過重連解決,使用者永遠出不去)。

timeout 是 connecting 的隱形退出路徑:取消靠使用者主動,timeout 是系統兜底 — 取值略大於 client / 後端的逾時設定(讓真正的失敗先回來、而非 UI 提前放棄),逾時後轉入 error 狀態走「重連 / 返回」。

畫面級回饋的檢查清單

  • 每個中間狀態能被使用者區辨(獨立畫面、或同畫面上明確的狀態指示)?
  • 每個中間狀態有退出路徑(對應畫面狀態矩陣)?
  • 等待狀態提供取消操作?
  • 錯誤 / 中斷狀態提供重試返回兩條路徑?
  • 長時間等待有 timeout 機制(不會永久卡在 connecting)?

反模式

反模式問題使用者行為
按鈕無任何視覺回饋使用者不確定有沒有按到重複點擊
非同步操作不禁用按鈕loading 期間仍可點擊重複提交(雙重扣款、重複建立)
loading 結束不恢復按鈕操作完成但按鈕仍 disabled使用者以為介面當掉
只有 loading 無結果通知spinner 消失但畫面沒變化使用者不確定操作是否成功
同步按鈕無 debounce快速點擊觸發多次導航導航堆疊混亂(push 多次)
畫面中間狀態無退出路徑使用者被卡在 connecting只能殺 app(UX 死胡同)
錯誤狀態只有重連沒有返回問題無法重連解決時被鎖住在錯誤迴圈中反覆重連

表格第一行(按鈕無任何視覺回饋)的完整實證案例見 U.C5 匯出按鈕零回饋——狀態機完備但 UI 沒接線,三層回饋全缺。同族的兩個變體:佔位 handler 上線 — 按鈕存在但 onPressed 只接開發提示或 log,dev toast 讓開發自測「有反應」、掩蓋未接線(U.C20);回饋文字被版面擠壓 — state 有值、綁定正確,flex 寬度競爭把「已選擇: 3/45」壓成「…」(U.C19)。回饋鏈要驗到使用者的眼睛,state 有值、widget 有綁都不是終點。

版面擠壓類問題有一個常見的誤歸因:專案用了等比縮放套件(如 flutter_screenutil),版面出事時第一反應是查套件設定。換算工具工作在常數層(設計稿座標 → 裝置座標的數值換算)、空間分配發生在 layout 協商層(有限寬度分給動態內容)— 兩層獨立,「有用響應式套件」不構成版面不會擠壓的保證。判斷式(換算錯 vs 分配錯)與三層修法(設計層空間競爭規格 / 實作層 flex 顯式決策 / 驗證層窄幕檢查)見 #228 等比縮放不管空間分配

設計檢查清單

為每個按鈕逐一確認:

  • 點擊瞬間有視覺回饋(ripple / 狀態變化 / 觸覺)?
  • 非同步操作:按鈕進入 disabled + loading 狀態?
  • 非同步操作:完成後按鈕恢復 idle?
  • 操作結果有明確通知(成功 / 失敗 / 空)?
  • 失敗時提供可執行的下一步(重試 / 替代方案)?
  • 同步按鈕有 debounce 防連點?

參考來源

本篇整理的設計原則來自以下公開可用性研究與設計系統:

  • Nielsen’s 10 Usability Heuristics(Nielsen Norman Group, 1994)— 啟發法 #1「系統狀態可見性」是回饋設計的理論基礎
  • Doherty Threshold(Doherty & Thadani, IBM 技術報告, 1982)— 400ms 回應時間門檻的原始研究
  • Jakob Nielsen’s Response Time Limits(1993)— 100ms / 1s / 10s 三門檻模型
  • Material Design 3: Interaction States(Google, m3.material.io)— 按鈕互動狀態定義(loading indicator 是 M3 的獨立元件、引用見時間感知與回應策略
  • Apple Human Interface Guidelines: Feedback(Apple Developer Documentation)— 回饋要可察覺、資訊明確並準確反映進度(本系列據此精神歸納出「即時、誠實的回應」的說法)
  • Laws of UX(Jon Yablonski, lawsofux.com)— Doherty Threshold 的現代整理與應用案例