How to Implement Hold‑to‑Delete with a Clip‑Path Overlay: A Complete CSS Guide

Hold‑to‑delete interactions use a CSS clip‑path: inset() overlay that slowly reveals over 2 seconds while pressed and snaps back in 200 ms when released.

This technique, pioneered in the emilkowalski/skills repository, provides deliberate, tactile feedback for destructive actions. Unlike width or background animations, clip‑path triggers no layout recalculation—making it performant and crisp at 60 fps.

How the Hold‑to‑Delete Pattern Works

The interaction relies on asymmetric timing: slow confirmation, fast cancellation. This matches how humans perceive risk—deliberation requires time, but aborting should feel instant.

The core mechanism uses a full‑size overlay clipped entirely on one edge. While the user holds the button, the clip contracts to reveal the overlay. Release early, and the clip snaps back.

Building the Component Structure

HTML Markup

<button class="delete-btn">
  Delete
  <span class="overlay"></span>
</button>

The .overlay element sits as a child, not a pseudo‑element, giving you maximum flexibility for JavaScript hooks or accessibility attributes.

CSS Foundation

/* Base button */
.delete-btn {
  position: relative;
  overflow: hidden;
  background: #e71d36;
  color: white;
  border: none;
  padding: 0.5rem 1rem;
  cursor: pointer;
  transition: transform 160ms ease-out;
}

/* Press feedback */
.delete-btn:active {
  transform: scale(0.97);
}

The overflow: hidden is critical—it contains the overlay within the button's rounded corners.

Implementing the Clip‑Path Animation

Overlay: Initial Hidden State

.delete-btn .overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.25);
  clip-path: inset(0 100% 0 0);  /* Hidden: clipped from right */
  transition: clip-path 200ms ease-out;  /* Fast abort */
}

inset(0 100% 0 0) means zero inset from top, bottom, and left—but 100% from the right. The overlay has zero visual presence.

Overlay: Active Reveal State

.delete-btn:active .overlay {
  clip-path: inset(0 0 0 0);  /* Fully visible */
  transition: clip-path 2s linear;  /* Slow, deliberate confirmation */
}

The specificity trick here matters: :active overrides the base transition exactly when needed. No JavaScript timing logic required.

Asymmetric Timing Explained

Phase Duration Easing Purpose
Hold (press) 2 s linear Forces user to commit; no acceleration reduces accidental triggers
Release (abort) 200 ms ease‑out Immediate feedback that action was cancelled

This pattern is documented in skills/review-animations/STANDARDS.md at lines 105–108, where the repository explicitly lists clip‑path inset as the technique for hold‑to‑delete overlays and specifies this exact timing asymmetry.

Directional Variants

Change the clip direction by adjusting which inset() value is 100%:

/* Fill from left */
clip-path: inset(0 0 0 100%);
clip-path: inset(0 0 0 0);  /* Active */

/* Fill from top */
clip-path: inset(100% 0 0 0);
clip-path: inset(0 0 0 0);  /* Active */

/* Fill from center (iris effect) */
clip-path: inset(50%);
clip-path: inset(0);  /* Active */

The repository's examples in skills/emil-design-eng/SKILL.md favor left‑to‑right fills for destructive actions, matching Western reading patterns and progress‑bar conventions.

Adding Completion Handling

Pure CSS cannot detect when the 2‑second hold completes. Pair with minimal JavaScript:

const btn = document.querySelector('.delete-btn');
let holdTimer;

btn.addEventListener('mousedown', () => {
  holdTimer = setTimeout(() => {
    btn.dispatchEvent(new Event('holdcomplete'));
  }, 2000);
});

btn.addEventListener('mouseup', () => clearTimeout(holdTimer));
btn.addEventListener('mouseleave', () => clearTimeout(holdTimer));

btn.addEventListener('holdcomplete', () => {
  // Actually delete
  console.log('Deleted!');
});

Touch events require parallel touchstart/touchend handlers. The visual feedback remains CSS‑only; JavaScript only handles the final action.

Accessibility Considerations

  • ARIA: Add aria-label="Hold 2 seconds to delete" for screen reader context
  • Focus indicators: Ensure :focus-visible styling remains distinct from :active
  • Reduced motion: Respect prefers-reduced-motion by disabling or accelerating the transition
@media (prefers-reduced-motion: reduce) {
  .delete-btn .overlay,
  .delete-btn:active .overlay {
    transition: none;
  }
  .delete-btn[aria-pressed="true"] .overlay {
    clip-path: inset(0 0 0 0);
  }
}

Performance Characteristics

clip‑path animates on the GPU's compositor thread, avoiding:

  • Layout thrashing (unlike width/height)
  • Paint invalidation of parent elements (unlike background‑position)
  • Main‑thread JavaScript timers

The skills/improve-animations/AUDIT.md file identifies clip‑path: inset() as a preferred reveal tool specifically for this performance profile.

Summary

  • The hold‑to‑delete pattern uses clip‑path: inset() to animate an overlay's visibility
  • Asymmetric timing—2 s linear on press, 200 ms ease‑out on release—creates deliberate, cancellable interactions
  • Source files STANDARDS.md (lines 105–108) and SKILL.md in emilkowalski/skills define this as the canonical implementation
  • No layout recalculation means 60 fps performance without will‑change hacks

Frequently Asked Questions

Why use clip‑path instead of animating width or transform: scaleX()?

clip‑path: inset() avoids layout recalculation and maintains the overlay's positioning without affecting sibling elements. width animations trigger reflow; scaleX distorts child content. clip‑path simply masks—content remains unscaled and crisp.

Can I use a pseudo‑element like ::before instead of a child span?

Yes, but child elements offer more flexibility. Pseudo‑elements cannot easily receive JavaScript state classes or ARIA attributes. The repository's examples in SKILL.md use actual child elements for this reason.

How do I detect when the user has held long enough to trigger the delete?

CSS alone cannot fire completion events. Use a 2000 ms setTimeout on mousedown/touchstart, clearing it on mouseup/touchend/mouseleave. The visual feedback remains pure CSS; JavaScript handles only the final action dispatch.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →