Aduok Code

Animated Delete Button with Pure HTML & CSS

Introduction

In this article we build TrashBtn — an animated delete button with a fully CSS-driven trash icon, a paper-shredding keyframe animation, and a checkmark confirmation badge. Clicking the button triggers a multi-step sequence: the label slides out, the trash icon centers and scales up, the lid tines splay open, a paper note slides down through the bin, and finally a checkmark badge pops in to confirm the deletion. The entire visual effect is CSS — only the class toggle is JavaScript.

All visual states are driven by CSS custom properties declared on .button. Toggling the .delete class on the element redefines those variables, and CSS transitions animate between the old and new values automatically. This pattern keeps the component fully themeable — every timing, color, and distance is a token in a single :root-level block on the element itself.

What you will learn

How to drive multi-step animations entirely from CSS custom property overrides — how to build a trash icon from pure CSS pseudo-elements with no SVG or images — how to use @keyframes to animate a paper note sliding through a bin — how to reveal and animate a checkmark badge with staggered transition-delay — how to keep button state in a CSS class and reset it from JavaScript with setTimeout.

How the Animation Works

The button has two visual layers: the .trash icon wrapper on the left and the <span> label on the right. In the idle state the icon is scaled down to 0.64 and offset left, making it appear as a small decorative prefix. When .delete is added, --trash-x: 46px slides the icon into the horizontal center of the button while --span-x: 16px and --span-opacity: 0 simultaneously slide the label out to the right and fade it away.

Every animated value — position, scale, opacity, timing — is a CSS custom property. The .delete class does nothing except redefine those variables. CSS transitions do the rest.

The paper and cut animations are the only true @keyframes in the component. They cannot be driven by custom property transitions because they involve multi-stop motion (the paper appears, rises, then falls). Everything else — icon movement, label fade, tine rotation, checkmark pop — is a CSS transition on a custom property value change.

01
Part One
Design Tokens

Step 1 — CSS Custom Properties

All color and state values are declared as custom properties directly on .button. This scopes the tokens to the component and avoids polluting the global :root. The state variables — --trash-x, --span-opacity, etc. — start at their idle defaults and are overridden when the .delete class is applied.

CSS
.button {
  /* colors */
  --background:       #2b3044;
  --background-hover: #1e2235;
  --text:             #fff;
  --shadow:           rgba(0, 9, 61, 0.2);
  --paper:            #5c86ff;
  --paper-lines:      #fff;
  --trash:            #e1e6f9;
  --trash-lines:      #bbc1e1;
  --check:            #fff;
  --check-background: #5c86ff;
}
Why declare tokens on .button instead of :root?

Declaring the tokens on .button rather than :root means you can place multiple differently-themed buttons on the same page by simply overriding the variables on a second selector. There is no global namespace to manage — each button instance is self-contained. It also makes the component trivially portable: copy the HTML and CSS block and it works anywhere with zero conflicts.

02
Part Two
HTML Structure

Step 2 — The Markup

The button contains two direct children: .trash (the animated icon) and <span> (the label). Inside .trash there are three children: .trash__lid which holds the paper note and forms the lid bar and tines via pseudo-elements, .trash__bin which is the body of the can, and .trash__check which is the confirmation badge that appears at the end of the sequence.

HTML
<button class="button" type="button" aria-label="Delete item">
  <div class="trash" aria-hidden="true">

    <div class="trash__lid">
      <!-- paper note that animates down into the bin -->
      <div class="trash__paper"></div>
    </div>

    <!-- bin body — ridges and paper strip via ::before/::after -->
    <div class="trash__bin"></div>

    <!-- success badge shown after deletion -->
    <div class="trash__check">
      <svg viewBox="0 0 8 6">
        <polyline points="1 3.4 2.71428571 5 7 1"></polyline>
      </svg>
    </div>

  </div>
  <span class="button__label">Delete Item</span>
</button>
ElementRoleAnimated
.buttonShell — holds all tokens and transitionsYes — scale, box-shadow, background
.trashIcon wrapper — translates and scales on deleteYes — translate + scale via custom props
.trash::before/afterLid tines — rotate outward when delete triggersYes — rotate 40deg on delete
.trash__lidLid area — overflow:hidden container for paperNo
.trash__lid::beforeHandle nub above the lid barNo
.trash__lid::afterLid bar — scaleX to 0 on deleteYes — scaleX transition
.trash__paperPaper note — keyframe animation drops it through the binYes — @keyframes paper
.trash__binBin body — border box with ridges and paper strip insideNo
.trash__bin::beforeVertical ridges — fade out on deleteYes — opacity transition
.trash__bin::afterPaper strip inside bin — @keyframes cut slides it throughYes — @keyframes cut
.trash__checkCheckmark badge — pops in with scale + opacity after 1.7sYes — scale + opacity + stroke-dashoffset
.button__labelText label — slides right and fades out on deleteYes — translateX + opacity
03
Part Three
Trash Icon

Step 3 — Building the Trash Icon from CSS

The trash icon is built entirely from borders, pseudo-elements, and box-shadows — no SVG, no images. The bin body is .trash__bin: a 20×25px div with a 2px border and border-radius: 1px 1px 4px 4px. The vertical ridges inside the bin are its ::before pseudo-element, using box-shadow to duplicate the strip: box-shadow: 10px 0 0 var(--trash-lines) creates a second ridge 10px to the right of the first with a single declaration.

CSS
/* Bin body */
.trash__bin {
  width: 20px;
  height: 25px;
  border: 2px solid var(--icon, var(--trash));
  border-radius: 1px 1px 4px 4px;
  position: relative;
  overflow: hidden;
  z-index: 2;
  transition: border-color 0.3s;
}

/* Vertical ridges — duplicated via box-shadow */
.trash__bin::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  width: 4px;
  height: 20px;
  border-radius: 2px;
  margin-left: -2px;
  background: var(--trash-lines);
  transform: translateX(-3px) scale(0.6);
  box-shadow: 10px 0 0 var(--trash-lines);
  opacity: var(--trash-lines-opacity, 1);
  transition: transform 0.4s, opacity 0.4s;
}

/* Lid tines — rotate outward on delete */
.trash::before,
.trash::after {
  content: '';
  position: absolute;
  bottom: 100%;
  width: 2px;
  height: 8px;
  border-radius: 1px;
  background: var(--icon, var(--trash));
  transform-origin: 50% 6px;
  transform: translate(var(--x, 3px), 2px) scaleY(var(--sy, 0.7)) rotate(var(--r, 0deg));
  transition: transform 0.4s, background 0.3s;
}

.trash::before { left: 1px; }
.trash::after  { right: 1px; --x: -3px; }
box-shadow as a shape duplicator

CSS box-shadow accepts multiple comma-separated shadows and they can have zero blur — making them crisp copies of the element at an offset. This is a useful trick for repeating shapes (ridges, dots, lines) without extra DOM elements. The ridge element is one <pseudo-element> producing two visual strips: itself and its shadow at 10px offset.

04
Part Four
Paper Animation

Step 4 — The Paper & Cut Keyframes

Two @keyframes sequences drive the paper shredding effect. The paper animation runs on .trash__paper — the small blue note above the bin lid. It starts hidden (opacity: 0), fades in, bounces up slightly, then falls down through the lid into the bin (translateY(24px)). The cut animation runs on .trash__bin::after — the paper strip inside the bin — sliding it down to simulate the paper passing through.

CSS
@keyframes paper {
  10%, 100% { opacity: 1; }
  20%       { transform: translateY(-16px); }
  40%       { transform: translateY(0); }
  70%, 100% { transform: translateY(24px); }
}

@keyframes cut {
  0%,  40% { transform: translate(-0.5px, -16px) scaleX(0.5); }
  100%     { transform: translate(-0.5px,  24px) scaleX(0.5); }
}

/* Applied on .delete */
.button.delete .trash__paper      { animation: paper 1.5s linear forwards 0.5s; }
.button.delete .trash__bin::after { animation: cut   1.5s linear forwards 0.5s; }

Both animations use animation-fill-mode: forwards so the paper stays in its final fallen position and does not snap back to the start when the animation ends.

05
Part Five
Checkmark Badge

Step 5 — The Confirmation Checkmark

The checkmark badge is an absolutely-positioned circle that sits below the bin at top: 24px. In the idle state it is invisible (--check-opacity: 0) and tiny (--check-scale: 0.2). When .delete is added, its opacity and scale animate in — but with --check-delay: 1.7s, so it only appears after the paper animation has completed. The SVG checkmark stroke draws itself via stroke-dashoffset going from 9px to 0 at --checkmark-delay: 2.1s.

CSS
.trash__check {
  padding: 4px 3px;
  border-radius: 50%;
  background: var(--check-background);
  position: absolute;
  left: 2px;
  top: 24px;
  opacity: var(--check-opacity, 0);
  transform: translateY(var(--check-y, 0)) scale(var(--check-scale, 0.2));
  transition:
    transform var(--check-duration,         0.2s) ease var(--check-delay, 0s),
    opacity   var(--check-duration-opacity,  0.2s) ease var(--check-delay, 0s);
}

.trash__check svg {
  display: block;
  width: 8px;
  height: 6px;
  fill: none;
  stroke: var(--check);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 9px;
  stroke-dashoffset: var(--check-offset, 9px);
  transition: stroke-dashoffset 0.4s ease var(--checkmark-delay, 0.4s);
}

/* Delete state — badge animates in at 1.7s */
.button.delete {
  --check-offset:          0;
  --check-opacity:         1;
  --check-scale:           1;
  --check-y:               16px;
  --check-delay:           1.7s;
  --checkmark-delay:       2.1s;
  --check-duration:        0.55s;
  --check-duration-opacity: 0.3s;
}
stroke-dashoffset for draw-on effects

Setting stroke-dasharray equal to the path length makes the entire stroke one dash. Setting stroke-dashoffset to the same value hides it completely — the dash is shifted off-screen. Animating stroke-dashoffset to 0 slides the dash into view, making the stroke appear to draw itself. This works for any SVG path: measure the path length with path.getTotalLength() and use that value for both properties.

06
Part Six
Delete State

Step 6 — The .delete Class Overrides

All animation state lives in the .button.delete rule block. It overrides the custom property defaults set on .button, which triggers every transition simultaneously. The label slides out, the icon translates to center, the tines rotate, and the staggered delays on the check badge ensure the confirmation only appears after the trash animation finishes.

CSS
.button.delete {
  /* label */
  --span-opacity: 0;
  --span-x:       16px;
  --span-delay:   0s;

  /* icon position */
  --trash-x:      46px;
  --trash-y:      2px;
  --trash-scale:  1;

  /* lid */
  --trash-lines-opacity: 0;
  --trash-line-scale:    0;
  --icon:                #fff;

  /* checkmark */
  --check-offset:           0;
  --check-opacity:          1;
  --check-scale:            1;
  --check-y:                16px;
  --check-delay:            1.7s;
  --checkmark-delay:        2.1s;
  --check-duration:         0.55s;
  --check-duration-opacity: 0.3s;
}

/* Tines splay open */
.button.delete .trash::before { --sy: 1; --x: 0; --r:  40deg; }
.button.delete .trash::after  { --sy: 1; --x: 0; --r: -40deg; }
07
Part Seven
JavaScript Toggle

Step 7 — The Class Toggle

The only JavaScript in the component is a click listener that adds .delete and removes it after 3200ms — long enough for the full animation sequence (paper: 0.5s delay + 1.5s duration = 2s, checkmark: 2.1s delay + 0.4s draw = ~2.5s, with margin). A guard clause prevents re-triggering while the animation is already running.

JavaScript
const ANIMATION_DURATION = 3200;

document.querySelectorAll('.button').forEach((button) => {
  button.addEventListener('click', (e) => {
    e.preventDefault();

    if (button.classList.contains('delete')) return;

    button.classList.add('delete');
    setTimeout(() => button.classList.remove('delete'), ANIMATION_DURATION);
  });
});
Why 3200ms for the reset timeout?

The last animation event is the checkmark stroke drawing at --checkmark-delay: 2.1s + 0.4s transition = 2.5s total. Adding ~700ms of breathing room gives 3200ms. If you shorten any of the delays in the CSS, reduce this value accordingly. If you lengthen them, increase it — otherwise the class will be removed before the animation finishes and the state will snap back mid-sequence.

Tuning Reference

Token / PropertyDefaultEffect
--trash-x46pxHow far the icon slides toward center on delete. Increase for wider buttons
--trash-scale0.64Idle icon scale. Increase to make the icon larger at rest
--span-x16pxHow far the label slides out to the right on delete
--check-delay1.7sWhen the checkmark badge starts appearing. Should be after the paper animation ends
--checkmark-delay2.1sWhen the SVG stroke starts drawing. Should be after the badge has scaled in
--check-duration0.55sHow long the badge scale-in takes
animation-delay (paper/cut)0.5sDelay before paper starts falling. Gives time for tines to splay open first
paper duration1.5sTotal duration of the paper falling animation
ANIMATION_DURATION (JS)3200msHow long before .delete is removed. Must exceed the last CSS animation end time
--background#2b3044Idle button background color
--background-hover#1e2235Hovered and active-delete button background
--paper#5c86ffColor of the paper note and paper strip inside bin
--check-background#5c86ffBackground color of the checkmark badge circle

Full Source Code

Save the following as delete-button.html and open in any modern browser. The only dependency is a Google Fonts import for Inter — remove it and add font-family: sans-serif to use system fonts instead.

HTML — delete-button.html (complete)
// Full source available in the Aduok GitHub repository
// https://github.com/aduok/aduok-code-snippets/blob/main/blog/delete-button.html

The label, the trash icon, the paper animation, and the checkmark — all driven by a single class toggle and zero CSS frameworks.

Want to see it in action?Watch the full build on YouTube