按鈕與切換

Hold to Confirm

長按填充確認按鈕:按住時進度由左到右填滿、接近完成時輕微震顫,填滿才觸發,防止危險操作誤觸。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add hold-to-confirm

還沒有專案,或安裝遇到問題?看快速開始 →

Hold to Confirm 是用 React、Tailwind CSS 與 Motion 做的長按確認按鈕(hold to confirm button):使用者必須按住滑鼠、手指或空白鍵直到填充跑滿才會觸發,適合用來取代「你確定嗎?」的確認對話框。

Playground

即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。

1.2
0
編輯中的呼叫預設屬性
<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型別預設值說明
holdDurationnumber1.2需按住多久(秒)才觸發確認,填充以此時長等速填滿
onConfirm(source: 'keyboard' | 'pointer') => void—填充填滿時觸發,每次成功只觸發一次;參數為觸發來源
confirmedChildrenReactNode—確認成功後顯示的內容,未提供時沿用 children
variant'danger' | 'default''danger'danger 紅系(危險操作用)、default 中性色
rollbackDurationnumber—提前放開時以此時長(毫秒)緩出回滾到 0;未設定時沿用 spring 回彈,建議 180
leaveTolerancenumber—pointer 移出按鈕超過此距離(px)才取消;未設定時一離開邊界就取消,建議 8
instantKeyboardbooleanfalse鍵盤 Enter/空白鍵按下即瞬間填滿並確認,不需按住
instantReducedMotionbooleanfalse系統開啟「減少動態效果」時,pointer 按下即瞬間填滿並確認
resetAfternumber—成功後經過此時長(毫秒)自動回到初始狀態;未設定時停在成功態,建議 1800
classNamestring—覆蓋預設樣式
childrenReactNode—按鈕內容

其餘 <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 讓誤觸可以復原。