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

> Learn to implement hold-to-delete using a CSS clip-path overlay. This guide details the animation for revealing and snapping back for a smooth user experience.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: how-to-guide
- Published: 2026-08-01

---

**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

```html
<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

```css
/* 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

```css
.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

```css
.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`](https://github.com/emilkowalski/skills/blob/main/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%`:

```css
/* 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`](https://github.com/emilkowalski/skills/blob/main/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:

```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

```css
@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`](https://github.com/emilkowalski/skills/blob/main/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`](https://github.com/emilkowalski/skills/blob/main/STANDARDS.md) (lines 105–108) and [`SKILL.md`](https://github.com/emilkowalski/skills/blob/main/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`](https://github.com/emilkowalski/skills/blob/main/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.