How to Implement a Hold-to-Delete Pattern: Complete Guide with CSS and JavaScript
The hold-to-delete pattern requires users to press and continuously hold a UI element for approximately 2 seconds before confirming a destructive action, utilizing a CSS clip-path overlay animation and JavaScript timer logic to prevent accidental deletions.
This interaction pattern adds a deliberate time-based safety barrier to destructive operations while providing immediate visual feedback. In the emilkowalski/skills repository, the implementation is standardized across animation and design engineering guidelines. This article explains how to build the exact pattern documented in skills/review-animations/STANDARDS.md and skills/emil-design-eng/SKILL.md, including the specific timing constants and visual techniques used in production.
How the Hold-to-Delete Pattern Works
The pattern consists of two coordinated systems working together. First, a visual overlay that progressively reveals itself using a clip-path transition to indicate that a deletion is in progress. Second, a timer mechanism that monitors the duration of the user's press and only commits the action after a sustained hold of approximately 2 seconds.
According to the source code in skills/emil-design-eng/SKILL.md, the interaction must sustain a 2 second linear fill animation while the user holds the element, followed by a 200 millisecond ease-out snap-back animation if the press is released early or the action completes【grep result 5†L589-L590】. This timing prevents accidental triggers while feeling responsive enough for intentional deletions.
CSS Implementation Using clip-path
The visual feedback relies on the clip-path: inset(t r b l) technique documented in skills/review-animations/STANDARDS.md【grep result 1†L135-L136】. This CSS property creates a mask that can animate from fully hidden to fully visible, creating the effect of the button being "eaten" by a progress overlay.
/* Container for the hold-to-delete button */
.hold-delete {
position: relative;
overflow: hidden;
cursor: pointer;
user-select: none;
}
/* The overlay that fills while holding */
.hold-delete::after {
content: "";
position: absolute;
inset: 0;
background: rgba(239, 68, 68, 0.2); /* Red tint for destructive action */
clip-path: inset(100% 0 0 0); /* Start completely hidden (bottom) */
transition: clip-path 2s linear;
pointer-events: none;
}
/* Active state triggered by JavaScript */
.hold-delete.holding::after {
clip-path: inset(0 0 0 0); /* Fully reveal overlay */
}
/* Reset state for the 200ms snap-back */
.hold-delete:not(.holding)::after {
transition: clip-path 200ms ease-out;
}
The clip-path: inset() function defines a rectangular mask where the values represent top, right, bottom, and left offsets from the element's edges. Starting with inset(100% 0 0 0) hides the overlay entirely (100% from the top), while transitioning to inset(0 0 0 0) reveals it completely.
JavaScript Timer and State Management
The logic layer uses Pointer Events to handle both mouse and touch interactions consistently. The implementation tracks the hold duration using setTimeout and manages three distinct states: idle, holding, and success.
function enableHoldToDelete(buttonElement, onDeleteCallback) {
let holdTimer = null;
const HOLD_DURATION = 2000; // 2 seconds as per SKILL.md specification
const SNAP_BACK_DURATION = 200; // 200ms ease-out
const startHold = (event) => {
// Prevent default to avoid text selection on mobile
if (event.pointerType === 'touch') {
event.preventDefault();
}
buttonElement.classList.remove('success');
buttonElement.classList.add('holding');
// Start the deletion timer
holdTimer = setTimeout(() => {
completeHold();
}, HOLD_DURATION);
};
const completeHold = () => {
buttonElement.classList.remove('holding');
buttonElement.classList.add('success');
// Wait for the visual completion before executing deletion
setTimeout(() => {
buttonElement.classList.remove('success');
if (typeof onDeleteCallback === 'function') {
onDeleteCallback();
}
}, SNAP_BACK_DURATION);
};
const cancelHold = () => {
if (holdTimer) {
clearTimeout(holdTimer);
holdTimer = null;
}
buttonElement.classList.remove('holding');
// CSS transition handles the 200ms snap-back automatically
};
// Pointer events handle both mouse and touch
buttonElement.addEventListener('pointerdown', startHold);
buttonElement.addEventListener('pointerup', cancelHold);
buttonElement.addEventListener('pointerleave', cancelHold);
buttonElement.addEventListener('pointercancel', cancelHold);
}
This implementation respects the timing specifications found in skills/emil-design-eng/SKILL.md: the 2 second linear progression creates a deliberate pause, while the 200 millisecond ease-out provides a smooth visual reset when the user releases early or the action completes.
Integration and Usage Example
To implement this pattern in your application, attach the function to any button element and provide a callback for the actual deletion logic.
// Select the delete button for a specific item
const deleteButton = document.querySelector('[data-action="delete-item"]');
// Initialize the hold-to-delete behavior
enableHoldToDelete(deleteButton, () => {
// Your deletion logic here
const itemId = deleteButton.closest('.item').dataset.id;
// Remove from data model
deleteItemFromDatabase(itemId);
// Animate out of DOM
deleteButton.closest('.item').remove();
console.log(`Item ${itemId} deleted after 2s hold`);
});
For accessibility, ensure your button includes appropriate ARIA labels indicating that a long press is required:
<button
class="hold-delete"
data-action="delete-item"
aria-label="Hold for 2 seconds to delete this item"
>
Delete Item
</button>
Key Implementation Details from the Source Code
The emilkowalski/skills repository specifies several critical constraints for this pattern:
clip-path: inset()technique: Defined inskills/review-animations/STANDARDS.mdas the standard method for creating progress overlays that fill from one direction【grep result 1†L135-L136】.- 2 second minimum hold: Documented in
skills/emil-design-eng/SKILL.mdas the standard duration for destructive actions to prevent accidental triggers【grep result 5†L589-L590】. - 200 millisecond reset: The same file specifies the snap-back animation timing when the gesture is cancelled or completed.
- Visual cue standardization:
skills/find-animation-opportunities/SKILL.mdreferences theclip-path: insetapproach for indicating destructive actions requiring sustained interaction.
Summary
- The hold-to-delete pattern prevents accidental deletions by requiring a 2 second sustained press before executing destructive actions.
- Use
clip-path: inset()CSS animations to create a visual overlay that fills progressively while the user holds the button, as implemented inskills/review-animations/STANDARDS.md. - Implement Pointer Events (
pointerdown,pointerup,pointerleave) to handle both mouse and touch interactions with a single API. - Respect the 200 millisecond ease-out snap-back timing specified in
skills/emil-design-eng/SKILL.mdto provide smooth visual feedback when the interaction ends. - Always include clear ARIA labels and visual indicators** so users understand that a long press is required.
Frequently Asked Questions
How long should the hold duration be for a delete action?
According to the skills/emil-design-eng/SKILL.md source file, the standard hold duration is 2 seconds (2000ms) using a linear timing function. This duration is long enough to prevent accidental triggers during normal interaction while remaining short enough to not frustrate users performing intentional deletions.
Why use clip-path instead of width or transform for the overlay animation?
The clip-path: inset() technique is specified in skills/review-animations/STANDARDS.md because it allows the overlay to reveal from a specific direction (typically bottom-to-top) without affecting the layout or triggering expensive reflows. Unlike animating width or scale, clip-path performs efficiently on the compositor thread and provides cleaner visual masking for the "filling" effect characteristic of hold-to-delete interactions.
How do I handle touch devices and prevent text selection during the hold?
Use the Pointer Events API as shown in the implementation above, which unifies mouse and touch handling. Call event.preventDefault() within your pointerdown handler specifically for touch inputs, and apply user-select: none CSS to the button element. This prevents the browser's default text selection behavior while maintaining compatibility with screen readers and assistive technologies.
What happens if the user releases the button before 2 seconds?
If the user releases before the 2-second threshold, the JavaScript clears the timeout and removes the holding class, triggering the 200 millisecond ease-out transition defined in the CSS. This snaps the clip-path overlay back to its hidden state (inset(100% 0 0 0)), providing immediate visual feedback that the deletion was cancelled and preventing the callback from executing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →