Hold Button

A button that requires users to hold it down for a specified duration before triggering an action. Perfect for preventing accidental clicks on important operations like payments or deletions.

Hold Button

Create a file hold-button.tsx in your components folder and paste this code

'use client'; import { Check, Trash } from 'lucide-react'; import { AnimatePresence, motion } from 'framer-motion'; import { useState } from 'react'; const HoldButton = ({ children, variant = 'default', icon = null, onClick, holdDuration = 1000, successText = 'Confirmed' }: { children: React.ReactNode; variant?: 'default' | 'success' | 'destructive'; icon?: React.ReactNode; onClick?: () => void; holdDuration?: number; successText?: string; }) => { const [isSuccess, setIsSuccess] = useState(false); const [intervalId, setIntervalId] = useState<NodeJS.Timeout | null>(null); const handleMouseDown = () => { setIntervalId( setTimeout(() => { setIsSuccess(true); if (onClick) { onClick(); } setTimeout(() => setIsSuccess(false), 600); }, holdDuration) ); }; const handleMouseUp = () => { if (intervalId) { clearTimeout(intervalId); setIntervalId(null); } }; const variants = { default: { color: '#007BFF', backgroundColor: '#CCE5FF', icon: <Check size={18} /> }, success: { color: '#4CAF50', backgroundColor: '#C8E6C9', icon: <Check size={18} /> }, destructive: { color: '#E5484D', backgroundColor: '#FFDBDC', icon: <Trash size={18} /> } }; return ( <> <style> {` .button { position: relative; display: flex; height: 40px; align-items: center; gap: 8px; border-radius: 9999px; padding-inline: 24px; } .overlay-btn { position: absolute; top: 0px; clip-path: inset(0% 100% 0% 0%); } .wrapper { transition: 0.15s ease-out; } .wrapper:active .overlay-btn { clip-path: inset(0% 0% 0% 0%); transition: 1000ms ease-in-out; } .wrapper:active { scale: 0.97; } `} </style> <div onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} className="relative wrapper"> <button className="button trigger-btn hover:bg-zinc-800 bg-zinc-700 text-white"> {icon || variants[variant].icon} {children} </button> <button style={{ color: variants[variant].color, backgroundColor: variants[variant].backgroundColor, transition: intervalId ? `${holdDuration}ms ease-out` : '0.2s ease-out' }} className="button overlay-btn w-full" > <div className="shrink-0">{icon || variants[variant].icon}</div> <div className="relative w-full"> <AnimatePresence mode="popLayout"> {!isSuccess ? ( <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="whitespace-nowrap h-full" > {children} </motion.div> ) : ( <motion.p initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }} className="whitespace-nowrap h-full text-left" > {successText} </motion.p> )} </AnimatePresence> </div> </button> </div> </> ); }; export default HoldButton;

Usage

<div className="flex flex-col gap-4 items-center justify-center"> <HoldButton variant="default" onClick={() => { console.log('Confirmed'); }} > Hold to Confirm </HoldButton> <HoldButton variant="success" successText="Success" icon={<BadgeDollarSign size={18} />} > Hold to Pay Now </HoldButton> <HoldButton variant="destructive" successText="Deleted" holdDuration={2000}> Hold to Delete </HoldButton> </div>