互動回饋三層模型:點擊確認、等待指示、結果通知
核心觀念
介面設計中,使用者每執行一個動作,系統都必須提供回饋。回饋是可用性的基本要求。沒有回饋的按鈕,使用者無法區分「系統在處理」和「按鈕壞了」。
這個原則對應 Jakob Nielsen 十大可用性啟發法的第一條:系統狀態可見性(Visibility of System Status) — 系統應透過適當的回饋,在合理時間內告知使用者目前發生了什麼。
三層回饋模型
使用者按下按鈕後,回饋依時間順序分三層:
第一層:點擊確認(0-100ms)
系統確認「收到你的操作了」。
| 回饋形式 | 範例 | 適用場景 |
|---|---|---|
| 視覺狀態變化 | 按鈕顏色變深、凹陷效果 | 所有按鈕 |
| 水波紋效果 | Material Design ripple | 觸控介面 |
| 觸覺回饋 | 震動 | 行動裝置 |
| 聲音回饋 | 輕微點擊聲 | 無障礙輔助 |
設計原則:第一層回饋必須在 100ms 內出現。這個門檻來自人類感知研究 — 100ms 以內的延遲被感知為即時反應,超過後點擊與回饋之間開始出現可察覺的落差,回饋就失去「確認」的效果。平台原生元件的 pressed / ripple 互動態已滿足這個門檻,用平台預設互動態即自動合格;自建元件(自繪按鈕、canvas UI)才需要自行驗證回饋延遲。
第二層:等待指示(100ms - 10s)
系統告知「正在處理你的請求」。
| 等待時長 | 建議回饋 | 說明 |
|---|---|---|
| < 400ms | 不需額外指示 | Doherty Threshold:400ms 內完成、人機互動不需互相等待 |
| 400ms - 1s | Spinner / 按鈕 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 的現代整理與應用案例