Hold to Confirm
長按填充確認按鈕:按住時進度由左到右填滿、接近完成時輕微震顫,填滿才觸發,防止危險操作誤觸。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add hold-to-confirmHold to Confirm 是用 React、Tailwind CSS 與 Motion 做的長按確認按鈕(hold to confirm button):使用者必須按住滑鼠、手指或空白鍵直到填充跑滿才會觸發,適合用來取代「你確定嗎?」的確認對話框。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<HoldToConfirm/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- 刪除帳號、刪除專案、清空資料庫這類無法復原的操作。
- 取消訂閱、終止進行中的部署或批次作業。
- 管理後台中會影響其他人的動作,例如移除成員、撤銷 API 存取權。
- 行動版介面裡需要防止誤觸、又不想多跳一層對話框的按鈕。
- 不適合:需要顯示送出後載入與成功、失敗狀態的一般按鈕,改用 Stateful Button;想用拖曳手勢確認時,可以看 Slide to Unlock。
安裝
npx webberui@latest add https://webberui.com/r/hold-to-confirm.json安裝時會一併寫入 --wb-duration-fast CSS 變數到你的全域樣式。
使用
import { HoldToConfirm } from "@/components/ui/hold-to-confirm";
<HoldToConfirm
onConfirm={() => deleteAccount()}
confirmedChildren="已刪除"
>
按住刪除帳號
</HoldToConfirm>範例
呼叫 API,失敗時重置按鈕
元件填滿後會停在成功態。onConfirm 是同步回呼,可以在裡面發出請求,失敗時換一個 key 讓按鈕重新掛載回到初始狀態。
import * as React from "react";
import { HoldToConfirm } from "@/components/ui/hold-to-confirm";
export function DeleteProjectButton({ projectId }: { projectId: string }) {
const [attempt, setAttempt] = React.useState(0);
const [error, setError] = React.useState<string | null>(null);
return (
<div className="flex flex-col items-start gap-2">
<HoldToConfirm
key={attempt}
holdDuration={1.5}
confirmedChildren="專案已刪除"
onConfirm={async () => {
setError(null);
const res = await fetch(`/api/projects/${projectId}`, { method: "DELETE" });
if (!res.ok) {
setError("刪除失敗,請稍後再試");
setAttempt((n) => n + 1);
}
}}
>
按住刪除專案
</HoldToConfirm>
{error && <p className="text-sm text-red-600">{error}</p>}
</div>
);
}中性色變體
variant="default" 使用中性色,適合重要但不屬於破壞性的操作,例如發布或封存。
<HoldToConfirm variant="default" holdDuration={0.8} onConfirm={() => publishRelease()}>
按住發布 v2.4.0
</HoldToConfirm>搭配復原快餐條:回滾、容許距離、鍵盤瞬間確認
以下組合讓長按更順手:提前放開以 180ms 緩出回滾、手指滑出按鈕 8px 內不取消、鍵盤使用者按 Enter 或空白鍵直接確認,成功 1.8 秒後自動復位。onConfirm 會收到 "keyboard" 或 "pointer",可以拿來記錄或調整提示文字;搭配 Undo Snackbar 給 5 秒復原時間,危險操作仍有退路。
import { HoldToConfirm } from "@/components/ui/hold-to-confirm";
import { UndoSnackbar, useUndoSnackbar } from "@/components/ui/undo-snackbar";
export function DeleteWithUndo() {
const { show, snackbarProps } = useUndoSnackbar({ duration: 5000 });
return (
<>
<HoldToConfirm
rollbackDuration={180}
leaveTolerance={8}
instantKeyboard
instantReducedMotion
resetAfter={1800}
confirmedChildren="已刪除"
onConfirm={(source) => {
softDeleteProject();
show("已刪除 1 個專案", {
onUndo: () => restoreProject(),
onCommit: () => purgeProject(),
});
console.log("確認來源:", source);
}}
>
按住刪除專案
</HoldToConfirm>
<UndoSnackbar {...snackbarProps} />
</>
);
}以上新 props 預設都是關閉或未設定,不加就維持原本的 spring 回彈、離開邊界即取消、停在成功態等行為。
停用與自訂樣式
其他 button 屬性會直接透傳,className 會透過 cn 與預設樣式合併,可以覆寫寬度或圓角。
<HoldToConfirm
disabled={!canDelete}
aria-label="按住以移除成員"
className="w-full rounded-full"
onConfirm={() => removeMember(memberId)}
>
移除成員
</HoldToConfirm>Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
holdDuration | number | 1.2 | 需按住多久(秒)才觸發確認,填充以此時長等速填滿 |
onConfirm | (source: 'keyboard' | 'pointer') => void | — | 填充填滿時觸發,每次成功只觸發一次;參數為觸發來源 |
confirmedChildren | ReactNode | — | 確認成功後顯示的內容,未提供時沿用 children |
variant | 'danger' | 'default' | 'danger' | danger 紅系(危險操作用)、default 中性色 |
rollbackDuration | number | — | 提前放開時以此時長(毫秒)緩出回滾到 0;未設定時沿用 spring 回彈,建議 180 |
leaveTolerance | number | — | pointer 移出按鈕超過此距離(px)才取消;未設定時一離開邊界就取消,建議 8 |
instantKeyboard | boolean | false | 鍵盤 Enter/空白鍵按下即瞬間填滿並確認,不需按住 |
instantReducedMotion | boolean | false | 系統開啟「減少動態效果」時,pointer 按下即瞬間填滿並確認 |
resetAfter | number | — | 成功後經過此時長(毫秒)自動回到初始狀態;未設定時停在成功態,建議 1800 |
className | string | — | 覆蓋預設樣式 |
children | ReactNode | — | 按鈕內容 |
其餘 <button> 屬性(disabled、aria-* 等)皆直接透傳。點擊不會觸發確認——長按是唯一的確認手勢。
細節
- 等速填充:pointer 按下(或按住空白鍵)時,填充層以
scaleX從左到右線性填滿;提早放開時中途鬆手,再按下會從目前進度以相同速度續跑 - 臨界震顫:進度超過 65% 後按鈕開始微幅左右抖動,振幅隨進度放大,為「即將生效」加強張力
- spring 回退:提早放開時填充以 spring(
stiffness: 500, damping: 42)快速彈回 0;設定rollbackDuration後改為該時長的緩出回滾 - 容許距離:設定
leaveTolerance時會捕捉 pointer,手指稍微滑出按鈕仍持續填充,超過距離才取消 - 自動復位:設定
resetAfter時,成功態在該毫秒數後回到初始狀態,不必換key - 成功態:填滿即觸發
onConfirm,按鈕轉為綠色、勾勾以畫線動畫進場,並顯示confirmedChildren;成功後元件停在成功態,要重玩可換key重新掛載(見 demo) - 鍵盤支援:空白鍵 keydown 開始填充、keyup 取消,與滑鼠長按行為一致;
e.repeat已過濾,按住不會重複觸發。開啟instantKeyboard後 Enter 與空白鍵改為瞬間填滿並確認,onConfirm收到"keyboard" - 無障礙:以
aria-describedby掛上「按住以確認」的說明,成功後透過aria-live="polite"宣告「已確認」 - 使用者系統開啟「減少動態效果」時:按住時長與線性填充完全保留(功能不變),僅移除震顫,回退改為快速線性;開啟
instantReducedMotion則 pointer 按下即瞬間填滿並確認 touch-none+select-none避免觸控長按時觸發捲動與選字;pointer 離開按鈕或事件被取消(pointercancel)、鍵盤按住期間失焦,都會視為放開並取消
常見問題
直接點一下會觸發嗎?
預設不會。點擊與 Enter 都不會確認(開啟 instantKeyboard 時 Enter/空白鍵會直接確認),必須按住滑鼠左鍵、手指或空白鍵,直到 holdDuration 秒(預設 1.2)跑完才呼叫 onConfirm,提前放開會以 spring 彈回 0。
確認後怎麼回到初始狀態?
設定 resetAfter(例如 1800)即可在成功後自動復位。未設定時會停在綠色成功態,也可以換一個 key 讓它重新掛載。
可以在 Next.js App Router 使用嗎?
可以。原始碼開頭已有 "use client";它是用戶端元件,可以從 Server Component 頁面直接 import,onConfirm 裡也可以呼叫 Server Action。
鍵盤和螢幕閱讀器怎麼使用?
聚焦後按住空白鍵即可填充,放開或失焦會取消。按鈕以 aria-describedby 連到「按住以確認,提前放開會取消」的說明,成功後透過 aria-live="polite" 宣告「已確認」。
減少動態效果時會怎樣?
按住時長與線性填充保持不變,只移除接近完成時的震顫,提早放開時改為快速線性回退。若開啟 instantReducedMotion,按下即瞬間填滿並確認,建議搭配 Undo Snackbar 讓誤觸可以復原。