Delivery — ETA / Countdown Badge
A self-contained set of delivery ETA and countdown badges for tracking screens. It pairs a map-forward hero card with a live ticking countdown, an animated driver marker following an SVG route, and a full gallery of badge states — en-route countdown, arriving-now pulse, amber delayed alert, safety-green delivered confirmation, failed attempt, and a scheduled time-window pill. A live order feed shows independent mini countdowns flipping between states with toast notifications. Vanilla JS, no dependencies.
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;
--sh-1: 0 1px 2px rgba(22, 24, 29, 0.06), 0 4px 14px rgba(22, 24, 29, 0.06);
--sh-2: 0 10px 30px rgba(22, 24, 29, 0.1);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.wrap {
max-width: 960px;
margin: 0 auto;
padding: 28px 20px 64px;
}
/* ---------- Header ---------- */
.page-head {
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: 42px;
height: 42px;
border-radius: 12px;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
box-shadow: var(--sh-1);
}
.brand div { display: flex; flex-direction: column; line-height: 1.2; }
.brand strong { font-size: 16px; font-weight: 800; letter-spacing: -0.01em; }
.brand span { font-size: 12.5px; color: var(--muted); font-weight: 500; }
.ghost-btn {
font: inherit;
font-weight: 600;
font-size: 13px;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 9px 16px;
cursor: pointer;
transition: background .15s, transform .1s, border-color .15s;
}
.ghost-btn:hover { background: #fafafb; border-color: rgba(22,24,29,0.18); }
.ghost-btn:active { transform: scale(.97); }
.ghost-btn:focus-visible { outline: 3px solid rgba(91,141,239,.4); outline-offset: 2px; }
/* ---------- Hero card ---------- */
.board { margin-bottom: 30px; }
.hero-card {
display: grid;
grid-template-columns: 300px 1fr;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-2);
}
.hero-map {
position: relative;
background:
linear-gradient(rgba(255,255,255,.55), rgba(255,255,255,.55)),
repeating-linear-gradient(0deg, #e7eaf0 0 1px, transparent 1px 28px),
repeating-linear-gradient(90deg, #e7eaf0 0 1px, transparent 1px 28px),
#eef1f6;
min-height: 200px;
}
.route { position: absolute; inset: 0; width: 100%; height: 100%; }
.route-bg { fill: none; stroke: rgba(91,141,239,.22); stroke-width: 6; stroke-linecap: round; }
.route-fg {
fill: none; stroke: var(--track); stroke-width: 4; stroke-linecap: round;
stroke-dasharray: 1 1; stroke-dashoffset: 0;
}
.driver-marker { transition: transform .9s cubic-bezier(.4,.1,.2,1); }
.dm-ring { fill: rgba(255,90,44,.22); }
.dm-dot { fill: var(--brand); stroke: #fff; stroke-width: 2; }
.driver-marker .dm-ring { animation: dmPulse 1.8s ease-out infinite; transform-origin: center; }
@keyframes dmPulse {
0% { transform: scale(.5); opacity: .9; }
100% { transform: scale(1.9); opacity: 0; }
}
.pin path { fill: var(--brand-d); }
.pin-hole { fill: #fff; }
.hero-body { padding: 20px 22px 22px; display: flex; flex-direction: column; }
.hero-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
.order-id { font-size: 12.5px; font-weight: 700; color: var(--muted); letter-spacing: .02em; }
.hero-eta { margin: 0; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.hero-eta-num {
font-size: 52px; font-weight: 800; letter-spacing: -0.03em; line-height: 1;
font-variant-numeric: tabular-nums; color: var(--ink);
}
.hero-eta-cap { font-size: 13.5px; font-weight: 600; color: var(--muted); }
.hero-sub { margin: 10px 0 14px; color: var(--ink-2); font-size: 14px; }
.hero-window { margin-bottom: 18px; }
.window-pill {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12.5px; font-weight: 600; color: var(--ink-2);
background: var(--bg); border: 1px solid var(--line);
border-radius: 999px; padding: 6px 12px;
}
.window-pill svg { color: var(--track); }
.hero-actions { display: flex; gap: 6px; margin-top: auto; flex-wrap: wrap; }
.seg-btn {
font: inherit; font-size: 12.5px; font-weight: 600;
color: var(--ink-2); background: var(--surface);
border: 1px solid var(--line); border-radius: 999px;
padding: 8px 14px; cursor: pointer;
transition: all .15s;
}
.seg-btn:hover { border-color: rgba(22,24,29,.2); }
.seg-btn.is-active { background: var(--ink); color: #fff; border-color: var(--ink); }
.seg-btn:focus-visible { outline: 3px solid rgba(91,141,239,.4); outline-offset: 2px; }
/* ---------- Badges ---------- */
.badge {
display: inline-flex; align-items: center; gap: 8px;
font-size: 13px; font-weight: 700; letter-spacing: -0.005em;
padding: 7px 13px 7px 11px; border-radius: 999px;
border: 1px solid transparent; white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.badge-pulse {
width: 9px; height: 9px; border-radius: 50%; position: relative; flex: none;
background: currentColor;
}
.badge-pulse::after {
content: ""; position: absolute; inset: 0; border-radius: 50%;
background: currentColor; animation: badgePulse 1.6s ease-out infinite;
}
@keyframes badgePulse {
0% { transform: scale(1); opacity: .55; }
100% { transform: scale(3.2); opacity: 0; }
}
.badge[data-state="enroute"] { color: var(--track); background: rgba(91,141,239,.12); border-color: rgba(91,141,239,.28); }
.badge[data-state="enroute"] .badge-label { color: #2c4a8a; }
.badge[data-state="arriving"] { color: var(--brand); background: rgba(255,90,44,.13); border-color: rgba(255,90,44,.32); }
.badge[data-state="arriving"] .badge-label { color: var(--brand-d); }
.badge[data-state="arriving"] .badge-pulse::after { animation-duration: 1s; }
.badge[data-state="delayed"] { color: var(--warn); background: rgba(232,148,34,.14); border-color: rgba(232,148,34,.34); }
.badge[data-state="delayed"] .badge-label { color: #9a6410; }
.badge[data-state="delivered"] { color: var(--ok); background: rgba(31,157,98,.13); border-color: rgba(31,157,98,.32); }
.badge[data-state="delivered"] .badge-label { color: #0f6b41; }
.badge[data-state="failed"] { color: var(--danger); background: rgba(212,73,62,.13); border-color: rgba(212,73,62,.32); }
.badge[data-state="failed"] .badge-label { color: #9c2f27; }
.badge[data-state="window"] { color: var(--ink-2); background: var(--bg); border-color: var(--line); }
.badge[data-state="window"] .badge-label { color: var(--ink-2); }
/* ---------- State gallery ---------- */
.section-title { font-size: 15px; font-weight: 700; margin: 0 0 14px; letter-spacing: -0.01em; }
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
margin-bottom: 34px;
}
.state-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px;
box-shadow: var(--sh-1);
display: flex; flex-direction: column; gap: 12px;
transition: transform .14s, box-shadow .14s;
}
.state-card:hover { transform: translateY(-2px); box-shadow: var(--sh-2); }
.state-card .badge { align-self: flex-start; }
.state-meta { display: flex; flex-direction: column; gap: 2px; }
.state-meta strong { font-size: 13.5px; font-weight: 700; }
.state-meta span { font-size: 12.5px; color: var(--muted); }
/* ---------- Feed ---------- */
.feed-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.feed-row {
display: flex; align-items: center; gap: 14px;
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--r-md); padding: 13px 16px;
box-shadow: var(--sh-1);
transition: border-color .2s, background .3s;
}
.feed-row.flash { animation: rowFlash .7s ease; }
@keyframes rowFlash { 0% { background: #fff7f3; } 100% { background: var(--surface); } }
.feed-avatar {
width: 38px; height: 38px; border-radius: 50%; flex: none;
display: grid; place-items: center; font-weight: 700; font-size: 14px; color: #fff;
}
.feed-main { flex: 1 1 auto; min-width: 0; }
.feed-main b { font-size: 14px; font-weight: 700; display: block; }
.feed-main small { font-size: 12.5px; color: var(--muted); }
.feed-end { flex: none; }
/* ---------- Toast ---------- */
.toast-host {
position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px; align-items: center;
z-index: 50; pointer-events: none; width: max-content; max-width: 90vw;
}
.toast {
background: var(--ink); color: #fff;
font-size: 13px; font-weight: 600;
padding: 11px 18px; border-radius: 999px;
box-shadow: var(--sh-2);
display: flex; align-items: center; gap: 8px;
animation: toastIn .25s ease, toastOut .3s ease forwards 2.7s;
}
.toast .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--brand); }
@keyframes toastIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateY(8px); } }
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.grid { grid-template-columns: repeat(2, 1fr); }
.hero-card { grid-template-columns: 1fr; }
.hero-map { min-height: 160px; }
}
@media (max-width: 520px) {
.wrap { padding: 20px 14px 56px; }
.grid { grid-template-columns: 1fr; }
.hero-eta-num { font-size: 44px; }
.hero-actions { width: 100%; }
.seg-btn { flex: 1 1 auto; text-align: center; }
.ghost-btn { padding: 8px 12px; }
.feed-main small { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
}
@media (prefers-reduced-motion: reduce) {
*, *::after { animation: none !important; transition: none !important; }
}(function () {
"use strict";
// ---------- Toast helper ----------
var toastHost = document.getElementById("toastHost");
function toast(msg) {
if (!toastHost) return;
var el = document.createElement("div");
el.className = "toast";
el.innerHTML = '<span class="dot"></span><span></span>';
el.lastChild.textContent = msg;
toastHost.appendChild(el);
setTimeout(function () { el.remove(); }, 3100);
}
function pad(n) { return n < 10 ? "0" + n : "" + n; }
function fmtClock(date) {
var h = date.getHours(), m = date.getMinutes();
var ap = h >= 12 ? "PM" : "AM";
h = h % 12; if (h === 0) h = 12;
return h + ":" + pad(m) + " " + ap;
}
function fmtMMSS(totalSec) {
if (totalSec < 0) totalSec = 0;
var m = Math.floor(totalSec / 60), s = totalSec % 60;
return m + ":" + pad(s);
}
// ---------- Hero countdown engine ----------
var heroBadge = document.getElementById("heroBadge");
var heroBadgeLabel = document.getElementById("heroBadgeLabel");
var heroCountdown = document.getElementById("heroCountdown");
var heroCountdownCap = document.getElementById("heroCountdownCap");
var heroSub = document.getElementById("heroSub");
var heroWindow = document.getElementById("heroWindow");
var routeFg = document.getElementById("routeFg");
var driverMarker = document.getElementById("driverMarker");
var routeLen = routeFg.getTotalLength();
routeFg.style.strokeDasharray = routeLen;
var DEFAULT_SECONDS = 12 * 60;
var state = "enroute"; // enroute | arriving | delayed | delivered
var remaining = DEFAULT_SECONDS;
var routeStart = DEFAULT_SECONDS; // seconds at full distance
var tickHandle = null;
function setHeroBadge(stateName, label) {
heroBadge.setAttribute("data-state", stateName);
heroBadgeLabel.textContent = label;
var pulse = heroBadge.querySelector(".badge-pulse");
if (stateName === "delivered") {
if (pulse) pulse.style.display = "none";
} else if (pulse) {
pulse.style.display = "";
}
}
function placeDriver(progress) {
// progress 0..1 from start to destination
var p = Math.max(0, Math.min(1, progress));
var pt = routeFg.getPointAtLength(routeLen * p);
driverMarker.setAttribute("transform", "translate(" + pt.x + "," + pt.y + ")");
// remaining route in front of driver
routeFg.style.strokeDashoffset = routeLen * p;
}
function renderEnroute() {
var progress = 1 - remaining / routeStart;
placeDriver(progress);
if (remaining <= 0) {
transition("delivered", { auto: true });
return;
}
if (remaining <= 60 && state !== "arriving") {
state = "arriving";
setHeroBadge("arriving", "Arriving now");
heroCountdownCap.textContent = "driver is nearby";
heroSub.textContent = "Marisol G. is pulling up — meet at the lobby door.";
toast("Your driver is arriving now");
} else if (remaining > 60 && state !== "delayed") {
state = "enroute";
setHeroBadge("enroute", "En route");
heroCountdownCap.textContent = "until arrival";
}
heroCountdown.textContent = fmtMMSS(remaining);
}
function startTicking() {
if (tickHandle) return;
tickHandle = setInterval(function () {
if (state === "delivered" || state === "delayed") return;
remaining -= 1;
renderEnroute();
}, 1000);
}
function stopTicking() {
if (tickHandle) { clearInterval(tickHandle); tickHandle = null; }
}
function arrivedClock() {
var d = new Date();
d.setSeconds(0);
if (state !== "delivered") d.setMinutes(d.getMinutes() + Math.ceil(remaining / 60));
return d;
}
function transition(target, opts) {
opts = opts || {};
state = target;
if (target === "enroute") {
remaining = DEFAULT_SECONDS;
routeStart = DEFAULT_SECONDS;
setHeroBadge("enroute", "En route");
heroCountdownCap.textContent = "until arrival";
heroSub.textContent = "Marisol G. is on the way with your order from Cedar & Vine.";
heroCountdown.textContent = fmtMMSS(remaining);
placeDriver(0);
startTicking();
if (!opts.silent) toast("Tracking live — 12:00 to go");
} else if (target === "arriving") {
remaining = 45;
routeStart = 60;
setHeroBadge("arriving", "Arriving now");
heroCountdownCap.textContent = "driver is nearby";
heroSub.textContent = "Marisol G. is pulling up — meet at the lobby door.";
heroCountdown.textContent = fmtMMSS(remaining);
placeDriver(1 - remaining / routeStart);
startTicking();
if (!opts.silent) toast("Your driver is arriving now");
} else if (target === "delayed") {
remaining += 9 * 60;
routeStart = remaining;
stopTicking();
setHeroBadge("delayed", "+9 min late");
heroCountdownCap.textContent = "new estimate";
heroSub.textContent = "Heavy traffic on Lake Ave pushed the ETA back by about 9 minutes.";
heroCountdown.textContent = fmtMMSS(remaining);
placeDriver(0.45);
if (!opts.silent) toast("Delivery delayed by ~9 min");
} else if (target === "delivered") {
stopTicking();
var clk = fmtClock(new Date());
remaining = 0;
setHeroBadge("delivered", "Delivered " + clk);
heroCountdown.textContent = "Done";
heroCountdownCap.textContent = "left at your door";
heroSub.textContent = "Order handed off and photo proof saved. Enjoy your meal!";
placeDriver(1);
if (!opts.silent && !opts.auto) toast("Marked delivered at " + clk);
if (opts.auto) toast("Delivered — left at your door");
}
// update window pill text to stay coherent
if (target === "delivered") {
heroWindow.innerHTML = heroWindow.innerHTML.replace(/Window[^<]*/, "Delivered on time ✓");
}
// sync segmented control
segButtons.forEach(function (b) {
b.classList.toggle("is-active", b.getAttribute("data-jump") === (target === "delivered" || target === "delayed" || target === "arriving" ? target : "enroute"));
});
}
// ---------- Segmented control ----------
var segButtons = Array.prototype.slice.call(document.querySelectorAll(".seg-btn"));
segButtons.forEach(function (btn) {
btn.addEventListener("click", function () {
var jump = btn.getAttribute("data-jump");
transition(jump);
});
});
// ---------- Reset ----------
document.getElementById("resetBtn").addEventListener("click", function () {
heroWindow.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg> Window 6:10 – 6:30 PM';
transition("enroute");
toast("Demo reset");
});
// ---------- Live order feed (independent mini countdowns) ----------
var feedData = [
{ name: "Theo Markham", note: "2× Spicy ramen bowl", color: "#5b8def", base: 184, st: "enroute" },
{ name: "Priya Nandakumar", note: "Cold brew + croissant", color: "#1f9d62", base: 23, st: "arriving" },
{ name: "Wren Castillo", note: "Family pizza night", color: "#e89422", base: -340, st: "delayed" },
{ name: "Dele Okonkwo", note: "Grocery run, 14 items", color: "#d4493e", base: 0, st: "delivered" }
];
function badgeHtml(st, text, pulse) {
var p = pulse ? '<span class="badge-pulse" aria-hidden="true"></span>' : "";
return '<span class="badge" data-state="' + st + '">' + p + '<span class="badge-label">' + text + "</span></span>";
}
var feedList = document.getElementById("feedList");
feedData.forEach(function (row, i) {
var li = document.createElement("li");
li.className = "feed-row";
li.dataset.idx = i;
var initials = row.name.split(" ").map(function (w) { return w[0]; }).join("").slice(0, 2);
li.innerHTML =
'<span class="feed-avatar" style="background:' + row.color + '">' + initials + "</span>" +
'<span class="feed-main"><b>' + row.name + "</b><small>" + row.note + "</small></span>" +
'<span class="feed-end"></span>';
feedList.appendChild(li);
});
function renderFeed() {
feedData.forEach(function (row, i) {
var li = feedList.children[i];
var end = li.querySelector(".feed-end");
var html;
if (row.st === "delivered") {
html = badgeHtml("delivered", "Delivered", false);
} else if (row.st === "delayed") {
html = badgeHtml("delayed", "+" + Math.ceil(Math.abs(row.base) / 60) + " min late", true);
} else if (row.base <= 0) {
// arriving -> flips to delivered
row.st = "delivered";
html = badgeHtml("delivered", "Delivered", false);
li.classList.add("flash");
setTimeout(function () { li.classList.remove("flash"); }, 700);
toast(row.name + "'s order delivered");
} else if (row.base <= 60) {
row.st = "arriving";
html = badgeHtml("arriving", "Arriving · " + fmtMMSS(row.base), true);
} else {
row.st = "enroute";
html = badgeHtml("enroute", fmtMMSS(row.base), true);
}
end.innerHTML = html;
});
}
setInterval(function () {
feedData.forEach(function (row) {
if (row.st !== "delivered" && row.st !== "delayed") row.base -= 1;
});
renderFeed();
}, 1000);
// ---------- Init ----------
renderFeed();
transition("enroute", { silent: true });
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Delivery — ETA / Countdown Badge</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="page-head">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 13h12V5H3zM15 8h4l3 3v2h-7z" />
<circle cx="7" cy="18" r="2" /><circle cx="18" cy="18" r="2" />
</svg>
</span>
<div>
<strong>Rondo Express</strong>
<span>ETA & Countdown Badges</span>
</div>
</div>
<button class="ghost-btn" id="resetBtn" type="button">Reset demo</button>
</header>
<section class="board" aria-label="Active delivery">
<article class="hero-card" aria-live="polite">
<div class="hero-map" aria-hidden="true">
<svg class="route" viewBox="0 0 320 180" preserveAspectRatio="none">
<path class="route-bg" d="M16 150 C 70 150, 80 60, 150 60 S 250 30, 304 30" />
<path class="route-fg" id="routeFg" d="M16 150 C 70 150, 80 60, 150 60 S 250 30, 304 30" />
<g id="driverMarker" class="driver-marker">
<circle r="11" class="dm-ring" />
<circle r="5" class="dm-dot" />
</g>
<g class="pin pin-end" transform="translate(304,30)">
<path d="M0 -14 C 8 -14, 8 -2, 0 6 C -8 -2, -8 -14, 0 -14 Z" />
<circle cy="-7" r="3.5" class="pin-hole" />
</g>
</svg>
</div>
<div class="hero-body">
<div class="hero-top">
<span class="badge" id="heroBadge" data-state="enroute">
<span class="badge-pulse" aria-hidden="true"></span>
<span class="badge-label" id="heroBadgeLabel">En route</span>
</span>
<span class="order-id">#RX-48217</span>
</div>
<h1 class="hero-eta">
<span class="hero-eta-num" id="heroCountdown">12:00</span>
<span class="hero-eta-cap" id="heroCountdownCap">until arrival</span>
</h1>
<p class="hero-sub" id="heroSub">Marisol G. is on the way with your order from Cedar & Vine.</p>
<div class="hero-window">
<span class="window-pill" id="heroWindow">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
Window 6:10 – 6:30 PM
</span>
</div>
<div class="hero-actions">
<button class="seg-btn is-active" data-jump="enroute" type="button">Live</button>
<button class="seg-btn" data-jump="arriving" type="button">Arriving</button>
<button class="seg-btn" data-jump="delayed" type="button">Delayed</button>
<button class="seg-btn" data-jump="delivered" type="button">Delivered</button>
</div>
</div>
</article>
</section>
<h2 class="section-title">Badge states</h2>
<section class="grid" aria-label="Badge state gallery">
<div class="state-card">
<span class="badge" data-state="enroute">
<span class="badge-pulse" aria-hidden="true"></span>
<span class="badge-label">8:42</span>
</span>
<div class="state-meta"><strong>Live countdown</strong><span>Ticks down every second toward the ETA.</span></div>
</div>
<div class="state-card">
<span class="badge" data-state="arriving">
<span class="badge-pulse" aria-hidden="true"></span>
<span class="badge-label">Arriving now</span>
</span>
<div class="state-meta"><strong>Arriving pulse</strong><span>Driver is within a block — strong pulse cue.</span></div>
</div>
<div class="state-card">
<span class="badge" data-state="delayed">
<span class="badge-pulse" aria-hidden="true"></span>
<span class="badge-label">+9 min late</span>
</span>
<div class="state-meta"><strong>Delayed</strong><span>Amber alert state when ETA slips.</span></div>
</div>
<div class="state-card">
<span class="badge" data-state="delivered">
<span class="badge-label">Delivered 6:18 PM</span>
</span>
<div class="state-meta"><strong>Delivered</strong><span>Safety-green confirmation, pulse off.</span></div>
</div>
<div class="state-card">
<span class="badge" data-state="failed">
<span class="badge-label">Attempt failed</span>
</span>
<div class="state-meta"><strong>Failed attempt</strong><span>No-access drop, needs reschedule.</span></div>
</div>
<div class="state-card">
<span class="badge badge-window" data-state="window">
<span class="badge-label">Today, 4 – 6 PM</span>
</span>
<div class="state-meta"><strong>Time-window pill</strong><span>Scheduled deliveries without a live driver.</span></div>
</div>
</section>
<h2 class="section-title">Live order feed</h2>
<section class="feed" aria-label="Order feed">
<ul class="feed-list" id="feedList"></ul>
</section>
</div>
<div class="toast-host" id="toastHost" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>ETA / Countdown Badge
A compact badge system for delivery and logistics UIs, built around one idea: the ETA should be the loudest thing on the screen. The hero card leads with an oversized mm:ss countdown that ticks down every second, sitting on a CSS-grid map placeholder where an animated driver marker tracks along an SVG route toward a destination pin. A time-window pill and a status badge keep context one glance away.
The state gallery covers the full lifecycle: a live blue countdown, an orange arriving-now pulse, an amber +9 min late delayed alert, a safety-green delivered confirmation, a red failed-attempt pill, and a neutral scheduled time-window badge. Each badge uses a tabular-numeric font and a soft pulse dot so motion reads as “live” without being noisy. A segmented control lets you jump the hero between en-route, arriving, delayed, and delivered states to watch the transitions.
Below, a live order feed runs four independent mini countdowns that flip themselves from en-route to arriving to delivered, flashing the row and firing a toast() when an order lands. Everything is vanilla JS in a single IIFE — drop the three snippet files in and the badges drive themselves, with prefers-reduced-motion honored throughout.
Illustrative UI only — fictional brand, not a real delivery service.