Delivery — Status Step Tracker
A reusable delivery status step tracker in horizontal and vertical variants, walking a parcel through placed, confirmed, out for delivery and delivered. Features an animated active step with a pulsing marker, green completed checks, live timestamps, a shrinking ETA and arrival clock, a courier card that appears only in transit, status pills, and buttons to advance, step back or reset the whole flow.
MCP
Code
:root {
--brand: #ff5a2c;
--brand-d: #e0461d;
--ink: #16181d;
--ink-2: #3b3f4a;
--muted: #71757f;
--bg: #f4f5f7;
--surface: #ffffff;
--line: rgba(22, 24, 29, 0.1);
--ok: #1f9d62;
--warn: #e89422;
--danger: #d4493e;
--track: #5b8def;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-sm: 0 1px 2px rgba(22, 24, 29, 0.06), 0 1px 3px rgba(22, 24, 29, 0.05);
--shadow-md: 0 6px 20px rgba(22, 24, 29, 0.08), 0 2px 6px rgba(22, 24, 29, 0.05);
}
* { box-sizing: border-box; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 480px at 12% -8%, rgba(91, 141, 239, 0.1), transparent 60%),
radial-gradient(900px 420px at 100% 0%, rgba(255, 90, 44, 0.08), transparent 55%),
var(--bg);
min-height: 100vh;
}
.wrap {
max-width: 980px;
margin: 0 auto;
padding: 28px 20px 56px;
}
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: var(--r-md);
background: linear-gradient(145deg, var(--brand), var(--brand-d));
color: #fff;
box-shadow: var(--shadow-sm);
}
.brand-name { margin: 0; font-weight: 800; font-size: 17px; letter-spacing: -0.01em; }
.brand-sub { margin: 0; font-size: 12px; color: var(--muted); font-weight: 500; }
.track-id {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.track-id-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); font-weight: 600; }
.track-id-num { font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; }
/* Layout */
.grid {
display: grid;
grid-template-columns: 1.35fr 1fr;
gap: 20px;
align-items: start;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: var(--shadow-md);
}
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 22px;
}
.card-title { margin: 0; font-size: 15px; font-weight: 700; }
.muted-line { font-size: 13px; color: var(--muted); font-weight: 500; font-variant-numeric: tabular-nums; }
.eta-status { margin: 0 0 4px; font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
.eta-line { margin: 0; font-size: 14px; color: var(--ink-2); }
.eta-line strong { color: var(--brand-d); font-variant-numeric: tabular-nums; font-size: 16px; }
/* Pills */
.pill {
flex: none;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 11px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pill-track[data-state="placed"] { color: var(--muted); background: rgba(113,117,127,0.12); }
.pill-track[data-state="confirmed"] { color: var(--warn); background: rgba(232,148,34,0.14); }
.pill-track[data-state="out"] { color: var(--track); background: rgba(91,141,239,0.14); }
.pill-track[data-state="delivered"] { color: var(--ok); background: rgba(31,157,98,0.14); }
/* Stepper shared */
.stepper { list-style: none; margin: 0; padding: 0; }
.step .dot {
position: relative;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--surface);
border: 2px solid var(--line);
color: #fff;
transition: border-color 0.35s ease, background 0.35s ease, box-shadow 0.35s ease, transform 0.35s ease;
}
.step .check {
width: 9px; height: 5px;
border-left: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(-45deg) scale(0);
transition: transform 0.3s ease 0.05s;
margin-top: -2px;
}
.step .step-name { font-weight: 700; font-size: 13.5px; color: var(--ink-2); transition: color 0.3s ease; }
.step .step-time { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.step .step-desc { font-size: 12.5px; color: var(--muted); }
/* completed */
.step.is-done .dot { background: var(--ok); border-color: var(--ok); color: #fff; }
.step.is-done .check { transform: rotate(-45deg) scale(1); }
.step.is-done .step-name { color: var(--ink); }
/* active */
.step.is-active .dot {
background: var(--brand);
border-color: var(--brand);
box-shadow: 0 0 0 5px rgba(255, 90, 44, 0.16);
animation: pulse 1.8s ease-in-out infinite;
}
.step.is-active .step-name { color: var(--brand-d); }
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 5px rgba(255, 90, 44, 0.16); }
50% { box-shadow: 0 0 0 9px rgba(255, 90, 44, 0.05); }
}
/* Horizontal stepper */
.stepper-h {
display: grid;
grid-template-columns: repeat(4, 1fr);
position: relative;
margin-bottom: 26px;
}
.stepper-h .step {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 8px;
position: relative;
padding-top: 4px;
}
.stepper-h .step .dot { width: 30px; height: 30px; z-index: 1; }
/* connector line */
.stepper-h .step::before {
content: "";
position: absolute;
top: 19px;
left: -50%;
width: 100%;
height: 3px;
background: var(--line);
z-index: 0;
transition: background 0.4s ease;
}
.stepper-h .step:first-child::before { display: none; }
.stepper-h .step.is-done::before,
.stepper-h .step.is-active::before { background: linear-gradient(90deg, var(--ok), var(--ok)); }
.stepper-h .step-body { display: flex; flex-direction: column; gap: 1px; }
/* Vertical stepper */
.stepper-v .step {
display: grid;
grid-template-columns: 30px 1fr;
gap: 14px;
position: relative;
padding-bottom: 22px;
}
.stepper-v .step:last-child { padding-bottom: 0; }
.stepper-v .step .dot { width: 28px; height: 28px; margin-top: 1px; }
.stepper-v .step::before {
content: "";
position: absolute;
left: 13px;
top: 30px;
bottom: -1px;
width: 3px;
background: var(--line);
transition: background 0.4s ease;
}
.stepper-v .step:last-child::before { display: none; }
.stepper-v .step.is-done::before { background: var(--ok); }
.stepper-v .step-body { display: flex; flex-direction: column; gap: 2px; padding-top: 1px; }
/* Controls */
.controls { display: flex; gap: 10px; flex-wrap: wrap; }
.btn {
appearance: none;
font-family: inherit;
font-weight: 700;
font-size: 13.5px;
border-radius: var(--r-sm);
padding: 10px 16px;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.08s ease, background 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 3px solid rgba(91,141,239,0.45); outline-offset: 2px; }
.btn-primary {
background: linear-gradient(145deg, var(--brand), var(--brand-d));
color: #fff;
box-shadow: var(--shadow-sm);
}
.btn-primary:hover { box-shadow: var(--shadow-md); }
.btn-primary:disabled { background: #d7d9de; color: #9aa0a6; cursor: not-allowed; box-shadow: none; }
.btn-ghost {
background: var(--surface);
border-color: var(--line);
color: var(--ink-2);
}
.btn-ghost:hover:not(:disabled) { background: #f7f8fa; color: var(--ink); }
.btn-ghost:disabled { color: #b6b9bf; cursor: not-allowed; }
/* Courier */
.courier {
display: flex;
align-items: center;
gap: 12px;
margin-top: 20px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: linear-gradient(180deg, rgba(91,141,239,0.05), transparent);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.courier.is-hidden { opacity: 0; transform: translateY(6px); pointer-events: none; height: 0; margin: 0; padding: 0; border: 0; overflow: hidden; }
.courier-av {
display: grid; place-items: center;
width: 40px; height: 40px;
border-radius: 50%;
background: var(--track);
color: #fff;
font-weight: 800;
font-size: 14px;
}
.courier-info { flex: 1; }
.courier-name { margin: 0; font-weight: 700; font-size: 14px; }
.courier-role { margin: 0; font-size: 12px; color: var(--muted); }
.courier-tag {
font-size: 12px;
font-weight: 700;
color: var(--ink-2);
background: rgba(22,24,29,0.05);
padding: 4px 10px;
border-radius: 999px;
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 18px);
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* Responsive */
@media (max-width: 760px) {
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.wrap { padding: 20px 14px 48px; }
.topbar { flex-wrap: wrap; }
.eta-status { font-size: 19px; }
.stepper-h .step-name { font-size: 12px; }
.stepper-h .step-time { font-size: 11px; }
.card { padding: 18px; }
.btn { flex: 1; text-align: center; }
}(function () {
"use strict";
var STEPS = [
{ key: "placed", label: "Order placed", state: "placed", pill: "Order placed" },
{ key: "confirmed", label: "Confirmed & packed", state: "confirmed", pill: "Preparing" },
{ key: "out", label: "Out for delivery", state: "out", pill: "In transit" },
{ key: "delivered", label: "Delivered", state: "delivered", pill: "Delivered" }
];
// Base timeline anchored to "now" so timestamps feel live.
var BASE = new Date();
BASE.setHours(BASE.getHours() - 3, 14, 0, 0);
var OFFSETS_MIN = [0, 41, 128, 174]; // minutes after BASE for each step
var current = 2; // start: out for delivery
var stepperH = document.getElementById("stepperH");
var stepperV = document.getElementById("stepperV");
var btnNext = document.getElementById("btnNext");
var btnBack = document.getElementById("btnBack");
var btnReset = document.getElementById("btnReset");
var statusEl = document.querySelector(".eta-status");
var pill = document.getElementById("state-pill");
var etaMin = document.getElementById("eta-min");
var etaClock = document.getElementById("eta-clock");
var courier = document.getElementById("courier");
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
function fmtTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var ampm = h >= 12 ? "PM" : "AM";
h = h % 12 || 12;
return h + ":" + (m < 10 ? "0" + m : m) + " " + ampm;
}
function timeForStep(i) {
var d = new Date(BASE.getTime());
d.setMinutes(d.getMinutes() + OFFSETS_MIN[i]);
return d;
}
// Paint timestamps once (only past/current steps show a real time).
function paintTimes(stepper) {
var lis = stepper.querySelectorAll(".step");
lis.forEach(function (li, i) {
var t = li.querySelector("[data-time]");
if (!t) return;
t.textContent = i <= current ? fmtTime(timeForStep(i)) : "Pending";
});
}
function applyClasses(stepper) {
var lis = stepper.querySelectorAll(".step");
lis.forEach(function (li, i) {
li.classList.toggle("is-done", i < current || (i === current && current === STEPS.length - 1));
li.classList.toggle("is-active", i === current && current < STEPS.length - 1);
});
}
function render(announce) {
[stepperH, stepperV].forEach(function (s) {
applyClasses(s);
paintTimes(s);
});
var step = STEPS[current];
statusEl.textContent = step.label;
pill.textContent = step.pill;
pill.setAttribute("data-state", step.state);
// ETA logic
if (current >= STEPS.length - 1) {
var d = timeForStep(current);
etaMin.parentElement.innerHTML = "Delivered at <strong>" + fmtTime(d) + "</strong>";
courier.classList.add("is-hidden");
} else {
var remaining = OFFSETS_MIN[STEPS.length - 1] - OFFSETS_MIN[current];
var arrive = new Date(Date.now() + remaining * 60000);
etaMin.textContent = remaining;
etaClock.textContent = "by " + fmtTime(arrive);
courier.classList.toggle("is-hidden", current < 2);
}
btnNext.disabled = current >= STEPS.length - 1;
btnBack.disabled = current <= 0;
btnNext.textContent = current >= STEPS.length - 2 ? "Mark delivered" : "Advance state";
if (announce) toast(step.label);
}
btnNext.addEventListener("click", function () {
if (current < STEPS.length - 1) {
current++;
render(true);
}
});
btnBack.addEventListener("click", function () {
if (current > 0) {
current--;
render(true);
}
});
btnReset.addEventListener("click", function () {
current = 0;
// re-anchor base so reset feels fresh
BASE = new Date();
BASE.setMinutes(BASE.getMinutes() - 1, 0, 0);
etaMin.parentElement.innerHTML =
'Arriving in <strong id="eta-min">' + OFFSETS_MIN[STEPS.length - 1] +
'</strong> min · <span id="eta-clock">—</span>';
etaMin = document.getElementById("eta-min");
etaClock = document.getElementById("eta-clock");
render(false);
toast("Tracker reset to Order placed");
});
render(false);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Delivery — Status Step Tracker</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="wrap">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="1" y="6" width="13" height="10" rx="1.5"/><path d="M14 9h4l3 3v4h-7"/><circle cx="6" cy="18" r="2"/><circle cx="17.5" cy="18" r="2"/>
</svg>
</span>
<div>
<p class="brand-name">Routely</p>
<p class="brand-sub">Parcel tracking</p>
</div>
</div>
<div class="track-id">
<span class="track-id-label">Tracking</span>
<span class="track-id-num">RT-48 209 117</span>
</div>
</header>
<main class="grid">
<!-- Primary card: horizontal tracker -->
<section class="card card-primary" aria-labelledby="primary-h">
<div class="card-head">
<div>
<h1 id="primary-h" class="eta-status">Out for delivery</h1>
<p class="eta-line">Arriving in <strong id="eta-min">26</strong> min · <span id="eta-clock">by 4:48 PM</span></p>
</div>
<span class="pill pill-track" id="state-pill" data-state="out">In transit</span>
</div>
<!-- horizontal stepper -->
<ol class="stepper stepper-h" id="stepperH" role="list" aria-label="Delivery progress">
<li class="step" data-step="0">
<span class="dot"><span class="check" aria-hidden="true"></span></span>
<span class="step-body">
<span class="step-name">Placed</span>
<span class="step-time" data-time>—</span>
</span>
</li>
<li class="step" data-step="1">
<span class="dot"><span class="check" aria-hidden="true"></span></span>
<span class="step-body">
<span class="step-name">Confirmed</span>
<span class="step-time" data-time>—</span>
</span>
</li>
<li class="step" data-step="2">
<span class="dot"><span class="check" aria-hidden="true"></span></span>
<span class="step-body">
<span class="step-name">Out for delivery</span>
<span class="step-time" data-time>—</span>
</span>
</li>
<li class="step" data-step="3">
<span class="dot"><span class="check" aria-hidden="true"></span></span>
<span class="step-body">
<span class="step-name">Delivered</span>
<span class="step-time" data-time>—</span>
</span>
</li>
</ol>
<div class="controls" role="group" aria-label="Advance delivery state">
<button class="btn btn-ghost" id="btnBack" type="button">Step back</button>
<button class="btn btn-primary" id="btnNext" type="button">Advance state</button>
<button class="btn btn-ghost" id="btnReset" type="button">Reset</button>
</div>
</section>
<!-- Secondary card: vertical tracker / activity log -->
<section class="card card-vert" aria-labelledby="vert-h">
<div class="card-head">
<h2 id="vert-h" class="card-title">Shipment activity</h2>
<span class="muted-line">2.1 mi away</span>
</div>
<ol class="stepper stepper-v" id="stepperV" role="list" aria-label="Detailed shipment timeline">
<li class="step" data-step="0">
<span class="dot"><span class="check" aria-hidden="true"></span></span>
<span class="step-body">
<span class="step-name">Placed</span>
<span class="step-desc">Order received — Maya R.</span>
<span class="step-time" data-time>—</span>
</span>
</li>
<li class="step" data-step="1">
<span class="dot"><span class="check" aria-hidden="true"></span></span>
<span class="step-body">
<span class="step-name">Confirmed</span>
<span class="step-desc">Packed at Eastgate hub</span>
<span class="step-time" data-time>—</span>
</span>
</li>
<li class="step" data-step="2">
<span class="dot"><span class="check" aria-hidden="true"></span></span>
<span class="step-body">
<span class="step-name">Out for delivery</span>
<span class="step-desc">Courier: Dev Okafor · Van 12</span>
<span class="step-time" data-time>—</span>
</span>
</li>
<li class="step" data-step="3">
<span class="dot"><span class="check" aria-hidden="true"></span></span>
<span class="step-body">
<span class="step-name">Delivered</span>
<span class="step-desc">Left at front door</span>
<span class="step-time" data-time>—</span>
</span>
</li>
</ol>
<div class="courier" id="courier">
<span class="courier-av" aria-hidden="true">DO</span>
<div class="courier-info">
<p class="courier-name">Dev Okafor</p>
<p class="courier-role">Your courier · ★ 4.9</p>
</div>
<span class="courier-tag">Van 12</span>
</div>
</section>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Status Step Tracker
A drop-in tracker for the post-checkout delivery screen, shown in two complementary variants. The primary card uses a horizontal stepper that moves through Placed, Confirmed, Out for delivery and Delivered, anchored by a big status headline and a live ETA that counts down the minutes and shows an arrival clock. The secondary card mirrors the same four states as a vertical activity timeline with hub, courier and drop-off detail lines.
The active step gets a pulsing orange marker, completed steps fill with a safety-green check and the connector line tints in as the parcel advances. A status pill swaps color and label for each state, and a courier card slides in only while the package is in transit, then hides itself on delivery.
Everything is keyboard-usable and driven by three controls: Advance state, Step back and Reset. Each transition repaints both steppers, updates timestamps and fires a toast — in production these clicks would be replaced by webhook updates from the carrier.
Illustrative UI only — fictional brand, not a real delivery service.