Hold to Confirm Button — Free Press and Hold JS Snippet
Hold to Confirm Button · Buttons · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="hc-wrap">
<button type="button" class="hc-btn" id="hcBtn">
<span class="hc-fill" id="hcFill"></span>
<span class="hc-label" id="hcLabel">Hold to delete</span>
</button>
<p class="hc-status" id="hcStatus">Press and hold for 1.2s</p>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0f172a;color:#e2e8f0;display:flex;justify-content:center;align-items:center;min-height:100vh;padding:24px}
.hc-wrap{text-align:center;width:100%;max-width:260px}
.hc-btn{position:relative;width:100%;border:none;border-radius:11px;background:#7f1d1d;color:#fff;font-family:inherit;font-size:14.5px;font-weight:800;padding:14px;cursor:pointer;overflow:hidden;user-select:none;-webkit-user-select:none;touch-action:none;transition:transform .1s}
.hc-btn:active{transform:scale(.985)}
.hc-fill{position:absolute;left:0;top:0;bottom:0;width:0;background:#dc2626;z-index:1}
.hc-label{position:relative;z-index:2;display:flex;align-items:center;justify-content:center;gap:7px}
.hc-btn.done{background:#16a34a}
.hc-btn.done .hc-fill{background:#16a34a}
.hc-status{margin-top:14px;font-size:12px;color:#94a3b8;font-weight:600;min-height:16px}var btn = document.getElementById('hcBtn');
var fill = document.getElementById('hcFill');
var label = document.getElementById('hcLabel');
var status = document.getElementById('hcStatus');
var HOLD_MS = 1200;
var startTime = 0;
var raf = null;
var done = false;
function frame(now) {
var elapsed = now - startTime;
var progress = Math.min(1, elapsed / HOLD_MS);
fill.style.width = (progress * 100) + '%';
if (progress >= 1) { confirm(); return; }
raf = requestAnimationFrame(frame);
}
function start() {
if (done) reset();
startTime = performance.now();
status.textContent = 'Keep holding\u2026';
raf = requestAnimationFrame(frame);
}
// Cancel if the pointer is released before the bar fills.
function cancel() {
if (done) return;
cancelAnimationFrame(raf);
raf = null;
fill.style.transition = 'width .2s';
fill.style.width = '0%';
status.textContent = 'Released too soon \u2014 try again';
setTimeout(function () { fill.style.transition = ''; }, 220);
}
function confirm() {
cancelAnimationFrame(raf);
done = true;
btn.classList.add('done');
fill.style.width = '100%';
label.textContent = 'Deleted';
status.textContent = 'Action confirmed';
}
function reset() {
done = false;
btn.classList.remove('done');
fill.style.width = '0%';
label.textContent = 'Hold to delete';
status.textContent = 'Press and hold for 1.2s';
}
btn.addEventListener('pointerdown', function (e) { btn.setPointerCapture(e.pointerId); start(); });
btn.addEventListener('pointerup', cancel);
btn.addEventListener('pointercancel', cancel);
btn.addEventListener('pointerleave', function () { if (raf) cancel(); });Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Hold to Confirm Button — Press-and-Hold Action

A hold-to-confirm button replaces a two-step "Are you sure?" dialog with a single deliberate gesture: the user presses and holds, a bar fills, and the action only fires when it completes. It's the safety pattern behind "hold to delete", "hold to power off", and destructive actions in apps that want intent without a modal interrupt. This snippet builds it in plain HTML, CSS, and vanilla JavaScript with no dependency.
Time-based progress, not a CSS transition
The fill is driven by requestAnimationFrame against performance.now(), not a fixed CSS animation. Each frame computes progress = elapsed / HOLD_MS and sets the fill width, so the bar's position always reflects exactly how long the button has been held — and releasing mid-way leaves it at the real point reached. Reaching progress >= 1 calls confirm() once and stops the loop, so the action can't double-fire.
Pointer Events with capture
A single set of Pointer Events covers mouse, touch, and pen. On pointerdown the button calls setPointerCapture, which means it keeps receiving pointerup even if the finger drifts off the button — critical on touch, where a small slide would otherwise drop the release event and leave the hold stuck. touch-action:none stops the page scrolling while you hold.
Cancel on early release
pointerup, pointercancel, and a guarded pointerleave all route to cancel(), which stops the animation frame and animates the fill back to zero with a brief CSS transition (re-enabled only for the retract so the fill-up itself stays frame-accurate). A status line gives feedback — "Released too soon" — so the gesture's requirement is obvious without trial and error.
Locked completion and reset
On success the button turns green, swaps its label to "Deleted", and sets done, which makes the next press reset first rather than re-running. Separating confirm(), cancel(), and reset() keeps each transition explicit and makes the control easy to reason about.
Wiring to a real action
Put your destructive call inside confirm() — delete the record, sign out, wipe the cache. Because completion is the single choke point, you get the safety of a held gesture with one line of integration, and you can tune HOLD_MS to match how dangerous the action is.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA red "Hold to delete" button renders with a status line beneath it.
- 2Press and holdA fill bar sweeps across the button while you keep pressing.
- 3Release earlyLetting go before it fills retracts the bar and shows a retry hint.
- 4Hold to completionAt full the button turns green and the action confirms once.
- 5Press again to resetA completed button resets on the next press before re-running.
- 6Wire your actionPut the real delete or sign-out call inside confirm().
Real-world uses
Common Use Cases
performance.now() inside requestAnimationFrame.Got questions?
Frequently Asked Questions
Because the progress must reflect the real hold duration so an early release leaves the bar exactly where it stopped. Each frame computes progress as elapsed / HOLD_MS from performance.now() and sets the width. A fixed CSS animation couldn't be paused at an arbitrary point or report how far it got when you let go.
It uses Pointer Events and calls setPointerCapture on pointerdown, so the button keeps receiving the pointerup even if your finger slides slightly off it — a common cause of stuck holds otherwise. touch-action:none also stops the page from scrolling while you press.
pointerup, pointercancel, and pointerleave all call cancel(), which stops the animation frame and animates the fill back to zero with a short transition, then shows a "Released too soon" message. The transition is enabled only for the retract so the fill-up itself stays frame-accurate.
No. When progress reaches 1, confirm() runs once and the animation loop stops. A done flag marks the button complete so the next press resets it rather than re-triggering, ensuring the destructive action executes a single time per hold.
Keep the progress and done state in the component and store the rAF id and start time in refs (not state) so updating them doesn't re-render. Attach the pointer handlers to a ref'd button, and call your action inside confirm(). Clean up the animation frame on unmount. In Tailwind, the fill is an absolutely-positioned span with an inline width bound to progress.