Aduok Code

Animated Delivery Truck Button with Pure HTML & CSS

Introduction

In this article we build DispatchBtn — an e-commerce order button with a fully animated delivery truck sequence built entirely from CSS keyframes and a single vanilla JS class toggle. When clicked, a package slides into the truck's cargo bay, the truck drives across the button, speed lines streak past, headlights ignite, and the label fades from "Dispatch Order" to "On Its Way ✓" — all inside the button's own 240×63px boundary. No frameworks, no SVG animation libraries, no canvas — just HTML, CSS custom properties, and 13 lines of JavaScript.

Every visual element — the truck cab, the cargo box, the speed lines, the windshield glare — is built from div elements and CSS pseudo-elements. The animation runs as a timed 10-second keyframe sequence choreographed across five separate @keyframes blocks. The entire component is re-themeable by editing a single :root block.

What you will learn

How to build a multi-element CSS animation inside a single button — how to create a pixel truck entirely from div elements and pseudo-elements — how to choreograph five independent keyframe animations on the same timeline — how to use overflow: hidden as a stage for off-screen actors — how to toggle CSS animations with a single JS class — how to use stroke-dashoffset for a check-mark draw-on effect — and how to structure all design tokens with CSS custom properties for a fully re-themeable component.

01
Part One
Design Tokens

Step 1 — CSS Custom Properties

All colour and palette values live in :root as custom properties. The button uses nine colour tokens. Swapping the blue cab for any other colour requires touching only --btn-blue.

CSS
:root {
  /* palette */
  --btn-bg:         #1C212E;   /* button dark background */
  --btn-blue:       #275EFE;   /* truck cab colour */
  --btn-blue-light: #7699FF;   /* windshield tint */
  --btn-grey:       #6C7486;   /* cab connector */
  --btn-grey-dark:  #3F4656;   /* connector gradient stop */
  --btn-grey-light: #CDD9ED;   /* cargo bay light stop */
  --btn-green:      #16BF78;   /* success check stroke */
  --btn-sand:       #DCB773;   /* package box dark stop */
  --btn-sand-light: #EDD9A9;   /* package box light stop */
}
02
Part Two
HTML Structure

Step 2 — The Markup

The button contains five child elements. Two <span> labels handle the idle/success text states. Three <div> elements — .cargo, .vehicle, and .rail — are the animated actors. The truck is further divided into .cab-back, .cab-front, .windshield, and two .headlight divs.

HTML
<button class="ship-btn" id="shipBtn">

  <!-- label: idle -->
  <span class="lbl idle">Dispatch Order</span>

  <!-- label: success (with SVG checkmark) -->
  <span class="lbl done">
    On Its Way
    <svg viewBox="0 0 12 10">
      <polyline points="1.5 6 4.5 9 10.5 1" />
    </svg>
  </span>

  <!-- animated package box -->
  <div class="cargo"></div>

  <!-- delivery truck -->
  <div class="vehicle">
    <div class="cab-back"></div>
    <div class="cab-front">
      <div class="windshield"></div>
    </div>
    <div class="headlight top"></div>
    <div class="headlight bottom"></div>
  </div>

  <!-- speed lines -->
  <div class="rail"></div>

</button>
ElementRoleAnimated
.ship-btnButton container — clips all actors via overflow: hiddenNo
.lbl.idleDefault label textYes — opacity fades out on .go
.lbl.doneSuccess label + SVG checkYes — opacity fades in, check draws on
.cargoPackage box, starts off left edgeYes — slides right into truck bay
.vehicleTruck wrapperYes — drives across the button
.vehicle::before/afterCargo bay door flapsYes — rotate open/closed
.headlight::beforeHeadlight beam glowYes — fades in as truck drives
.railSpeed lines (box-shadow trick)Yes — slides left as truck drives
03
Part Three
The Stage Trick

Step 3 — overflow: hidden as a Stage

The entire animation works because overflow: hidden on .ship-btn hides anything positioned outside its bounds. The truck starts at translateX(24px) — just past the right edge — and the cargo box starts at right: 100% — just past the left edge. Both are invisible until the animation moves them into frame, like actors waiting in the wings.

The button is not just a button — it is a stage. overflow: hidden is the curtain. Every actor enters from off-screen and exits to off-screen.

CSS
/* The button clips all out-of-bounds actors */
.ship-btn {
  position: relative;
  overflow: hidden;
  /* -webkit-mask ensures overflow:hidden works with border-radius */
  -webkit-mask-image: -webkit-radial-gradient(white, black);
}

/* Truck — starts off right edge, transform drives it left */
.vehicle {
  left: 100%;                  /* anchor at right edge */
  transform: translateX(24px); /* push 24px beyond right edge */
}

/* Cargo — starts off left edge */
.cargo {
  right: 100%;                 /* anchor at left edge */
  /* translateX animation moves it rightward into the button */
}
04
Part Four
Truck Animation

Step 4 — Choreographing the Truck

The veh keyframe drives the entire truck sequence in four acts across 10 seconds. At 10% it enters from the right and parks centre-left to receive the package. At 40% it pulls forward slightly — the bay doors close on the box. At 60% it drives fully off the left edge. From 75% it resets to the right, ready for the next click.

CSS
/* ── truck: 4-act drive sequence ── */
@keyframes veh {
  /* Act 1: enter from right, park at loading position */
  10%, 30% { transform: translateX(-164px); }

  /* Act 2: pull forward slightly — doors close on package */
  40%      { transform: translateX(-104px); }

  /* Act 3: drive off the left edge */
  60%      { transform: translateX(-224px); }

  /* Act 4: reset off right edge (instant jump, invisible) */
  75%, 100%{ transform: translateX(24px);  }
}

/* ── cargo door flaps: open to receive, close on package ── */
@keyframes hatch1 { 30%, 50% { transform: rotate(32deg);  } }
@keyframes hatch2 { 30%, 50% { transform: rotate(-32deg); } }

/* Apply with staggered delays so top door opens first */
.ship-btn.go .vehicle::before {
  animation: hatch1 2.4s ease forwards 0.3s;
}
.ship-btn.go .vehicle::after  {
  animation: hatch2 2.4s ease forwards 0.6s;
}
Why does the truck jump at 75%?

From 60% to 75% the truck is fully off the left edge — invisible. The keyframe uses this hidden window to teleport back to translateX(24px) (the starting position off the right edge) with no visible jump. It is a classic animation loop trick: reset during the blackout, not during the visible sequence.

05
Part Five
Package & Speed Lines

Step 5 — Package Box & Speed Lines

The .cargo box starts off the left edge and slides right as the truck parks. At 26% it abruptly becomes invisible — the package has been loaded. The speed lines use a pure CSS box-shadow trick: a single 6px-wide element with 22 box-shadow offsets creates 22 evenly-spaced lines at zero extra DOM cost.

CSS
/* ── package: slides in, disappears into truck bay ── */
@keyframes pkg {
  8%,  10% { transform: translateX(40px);  opacity: 1; }
  25%      { transform: translateX(112px); opacity: 1; }
  26%      { transform: translateX(112px); opacity: 0; } /* loaded */
  100%     { transform: translateX(0);     opacity: 0; }
}

/* ── speed lines: one element, 22 box-shadow clones ── */
.rail {
  opacity: 0;
  height: 3px; width: 6px;
  top: 30px; left: 100%;
  background: #fff;
  /* 22 copies spaced 15px apart, all white */
  box-shadow:
    15px 0 0 #fff, 30px 0 0 #fff, 45px 0 0 #fff, /* ... */
    330px 0 0 #fff;
}

@keyframes rails {
  0%,  30% { opacity: 0; transform: scaleY(.7) translateX(0);    }
  35%, 65% { opacity: 1;                                          }
  70%      { opacity: 0;                                          }
  100%     { transform: scaleY(.7) translateX(-400px);            }
}

Twenty-two speed lines from a single 6px div — the box-shadow trick lets you clone a shape across the button width at near-zero performance cost, since all shadows render in a single paint call.

06
Part Six
Headlights & Windshield

Step 6 — Headlights & Perspective Windshield

The headlight beams are ::before pseudo-elements on .headlight. They fade in as the truck pulls away using the glow keyframe. The windshield uses perspective(4px) rotateY(3deg) to create a subtle 3D tilt, making the flat shape read as a real angled glass surface. A transform-origin: 0 50% ensures the tilt pivots from the left edge, where it meets the cab body.

CSS
/* ── windshield: perspective tilt for 3D feel ── */
.windshield {
  background: var(--btn-blue-light);
  transform: perspective(4px) rotateY(3deg);
  transform-origin: 0 50%;   /* pivot from left seam */
  border-radius: 2px 8px 8px 2px;
}

/* ── headlight beam: fades in when truck drives ── */
.headlight::before {
  content: "";
  height: 4px; width: 7px;
  opacity: 0;   /* hidden until truck is moving */
  background: linear-gradient(90deg,
    rgba(240,220,95,1),
    rgba(240,220,95,.7),
    rgba(240,220,95,0)  /* fades to transparent */
  );
  transform: perspective(2px) rotateY(-15deg);
}

@keyframes glow {
  0%,  30% { opacity: 0; }
  40%, 100%{ opacity: 1; }
}
07
Part Seven
Labels & Checkmark

Step 7 — Label Transitions & SVG Checkmark Draw-On

The two labels share a --o CSS variable that drives opacity. On .go the idle label fades out immediately, and the done label fades in after a 7 second delay — just as the truck animation finishes. The SVG checkmark uses the classic stroke-dashoffset trick: the polyline has a stroke-dasharray equal to its own path length (16px), and animating stroke-dashoffset from 16 to 0 draws the stroke progressively.

CSS
/* ── idle label ── */
.lbl.idle      { transition-delay: .3s;  } /* slight delay on re-show */
.go .lbl.idle  { opacity: 0; transition-delay: 0s; }

/* ── done label: appears at 7s when truck is gone ── */
.lbl.done      { opacity: 0; }
.go .lbl.done  { opacity: 1; transition-delay: 7s; }

/* ── SVG checkmark: stroke-dashoffset draw-on ── */
.lbl.done svg {
  stroke: var(--btn-green);
  stroke-dasharray:  16px; /* total path length */
  stroke-dashoffset: 16px; /* fully hidden */
  transition: stroke-dashoffset .3s ease;
}
.go .lbl.done svg {
  stroke-dashoffset: 0;      /* draw the check */
  transition-delay: 7.3s;    /* 0.3s after label fades in */
}
08
Part Eight
The JavaScript

Step 8 — 13 Lines of Vanilla JS

The entire animation is triggered by adding a single class — .go — to the button. A guard prevents re-triggering mid-animation. A setTimeout removes the class after 10 seconds, resetting the button cleanly. No jQuery, no animation library, no state management.

JavaScript
const btn = document.getElementById('shipBtn');

btn.addEventListener('click', () => {
  // Guard: prevent re-triggering mid-animation
  if (btn.classList.contains('go')) return;

  // Start: add class triggers all CSS animations
  btn.classList.add('go');

  // Reset: remove class after animation completes (10s)
  setTimeout(() => {
    btn.classList.remove('go');
  }, 10_000);
});
Why not use animationend events?

The button has five simultaneous animations with different durations. animationend would fire five separate times on five different elements. Using a single setTimeout matching the longest animation (10 seconds) is simpler, more predictable, and avoids event delegation across the button's children.

Tuning Reference

Token / PropertyDefaultEffect
--btn-blue#275EFETruck cab colour. Change to retheme the whole truck
--btn-green#16BF78Checkmark stroke colour on success label
10s duration10sTotal animation length. Must match setTimeout value
translateX(-164px)-164pxTruck loading position. Adjust if button width changes
translateX(-224px)-224pxTruck exit position. Must exceed button width + truck width
transition-delay: 7s7sWhen success label appears. Sync with truck exit keyframe
box-shadow on .rail15px stepsSpeed line spacing. Tighter steps = denser lines
animation-delay hatch1/20.3s/0.6sStagger between top and bottom door opening
stroke-dashoffset delay7.3sCheck draw begins 0.3s after success label fades in

Full Source Code

Save the following as dispatch-button.html and open in any modern browser. Zero dependencies, zero build step, zero JavaScript frameworks.

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

Every effect in DispatchBtn — the truck drive, the package load, the speed lines, the headlight glow, the checkmark draw-on — is pure CSS keyframes. Not a single line of JavaScript touches the animation itself.

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