A hook for detecting long press interactions with visual progress feedback. Perfect for destructive actions that require user confirmation.
Installation
npm install betteruseUsage
import { useHold } from 'betteruse'
function DeleteButton() {
const { handlers, progress, isHolding } = useHold({
duration: 500,
onStart: () => console.log('Hold started'),
onComplete: () => deleteItem(),
onCancel: () => console.log('Hold cancelled'),
})
return (
<button {...handlers}>
{isHolding
? `Deleting... ${Math.round(progress * 100)}%`
: 'Hold to Delete'}
</button>
)
}
API Reference
useHold(options)
| Prop | Type | Default | Description |
|---|---|---|---|
duration | number | 500 | Hold duration in milliseconds |
onStart | () => void | - | Callback when hold starts |
onComplete | () => void | - | Callback when hold completes successfully |
onCancel | () => void | - | Callback when hold is cancelled |
moveThreshold | number | 10 | Touch move threshold in pixels before cancelling |
Return Value
| Prop | Type | Default | Description |
|---|---|---|---|
handlers | object | - | Event handlers to spread on target element |
progress | number | - | Progress from 0 to 1 |
isHolding | boolean | - | Whether currently holding |
handlers Object
The handlers object contains the following event handlers:
onMouseDown- Mouse press startonMouseUp- Mouse releaseonMouseLeave- Mouse leaves elementonTouchStart- Touch startonTouchEnd- Touch endonTouchMove- Touch move (for threshold detection)
How It Works
- User presses and holds the element
onStartcallback fires- Progress updates from 0 to 1 using
requestAnimationFrame - If user releases early or moves (touch),
onCancelfires - If hold completes,
onCompletefires
This hook is SSR-safe and uses requestAnimationFrame for smooth progress updates.
Examples
With Progress Bar
function HoldToDelete() {
const { handlers, progress, isHolding } = useHold({
duration: 1000,
onComplete: () => handleDelete(),
})
return (
<button {...handlers} className="relative overflow-hidden">
{isHolding && (
<div
className="absolute inset-0 bg-red-500/20"
style={{ width: `${progress * 100}%` }}
/>
)}
<span className="relative">Hold to Delete</span>
</button>
)
}
Confirm Action
function ConfirmSend() {
const { handlers, progress, isHolding } = useHold({
duration: 800,
onComplete: () => sendMessage(),
})
return (
<button {...handlers}>
{isHolding ? (
<span>Sending in {Math.ceil((1 - progress) * 0.8)}s...</span>
) : (
<span>Hold to Send</span>
)}
</button>
)
}
With Haptic Feedback
function HapticHoldButton() {
const { handlers, progress, isHolding } = useHold({
duration: 500,
onStart: () => {
// Trigger haptic feedback on supported devices
navigator.vibrate?.(10)
},
onComplete: () => {
navigator.vibrate?.(50)
performAction()
},
})
return <button {...handlers}>Hold to Confirm</button>
}
Accessibility
For better accessibility, consider adding:
<button
{...handlers}
aria-label="Hold for 500 milliseconds to delete"
role="button"
>
Hold to Delete
</button>