Clinic — Pharmacy Delivery Tracker
A pharmacy prescription delivery tracker card with a four-stage status stepper, a live ETA panel, an out-for-delivery courier card, and an order-items list with delivery address — all driven by a small vanilla-JS state machine.
MCP
代码
:root {
--teal: #129c93;
--teal-d: #0c7a73;
--teal-700: #0a655f;
--teal-50: #e7f5f3;
--coral: #ff7a66;
--coral-soft: #ffe6df;
--ink: #16322f;
--ink-2: #3a534f;
--muted: #6b827e;
--bg: #f1f7f6;
--white: #ffffff;
--line: rgba(16, 50, 47, 0.1);
--line-2: rgba(16, 50, 47, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--pending: #6b7280;
--danger: #d4503e;
--font: "Inter", system-ui, -apple-system, sans-serif;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(16, 50, 47, 0.05), 0 4px 14px rgba(16, 50, 47, 0.06);
--shadow-2: 0 16px 40px rgba(12, 122, 115, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
/* ── Layout ── */
.tracker {
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 20px;
}
.card {
width: 100%;
max-width: 460px;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-1);
padding: 24px 24px 22px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* ── Header ── */
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.eyebrow {
font-size: 0.74rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--teal-d);
}
.card-head h1 {
font-size: 1.4rem;
font-weight: 800;
letter-spacing: -0.02em;
margin: 3px 0 2px;
}
.sub {
color: var(--muted);
font-size: 0.86rem;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 7px;
border-radius: 999px;
padding: 6px 12px;
font-size: 0.78rem;
font-weight: 700;
white-space: nowrap;
background: var(--teal-50);
color: var(--teal-d);
}
.sp-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status-pill.is-prep {
background: rgba(217, 138, 43, 0.16);
color: var(--warn);
}
.status-pill.is-out {
background: var(--coral-soft);
color: var(--danger);
}
.status-pill.is-done {
background: rgba(47, 158, 111, 0.16);
color: var(--ok);
}
/* ── Stepper ── */
.stepper {
list-style: none;
display: flex;
flex-direction: column;
}
.step {
position: relative;
display: grid;
grid-template-columns: 26px 1fr;
column-gap: 14px;
padding-bottom: 22px;
}
.step:last-child {
padding-bottom: 0;
}
.step-rail {
position: absolute;
left: 12px;
top: 24px;
bottom: 0;
width: 2px;
background: var(--line-2);
}
.step:last-child .step-rail {
display: none;
}
.step-marker {
width: 26px;
height: 26px;
border-radius: 50%;
border: 2px solid var(--line-2);
background: var(--white);
display: grid;
place-items: center;
position: relative;
z-index: 1;
transition: background 0.2s, border-color 0.2s;
}
.step-title {
font-weight: 600;
font-size: 0.94rem;
color: var(--muted);
transition: color 0.2s;
}
.step-desc {
font-size: 0.82rem;
color: var(--muted);
margin-top: 2px;
}
.step-time {
font-size: 0.76rem;
font-weight: 600;
color: var(--ok);
margin-top: 5px;
}
/* completed */
.step.is-complete .step-rail {
background: var(--teal);
}
.step.is-complete .step-marker {
background: var(--teal);
border-color: var(--teal);
}
.step.is-complete .step-marker::after {
content: "✓";
color: #fff;
font-size: 0.78rem;
font-weight: 700;
}
.step.is-complete .step-title {
color: var(--ink);
}
/* current */
.step.is-current .step-marker {
border-color: var(--coral);
background: var(--coral-soft);
}
.step.is-current .step-marker::after {
content: "";
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--coral);
animation: pulse 1.4s ease-in-out infinite;
}
.step.is-current .step-title {
color: var(--ink);
font-weight: 700;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 122, 102, 0.5);
}
50% {
transform: scale(1.15);
box-shadow: 0 0 0 6px rgba(255, 122, 102, 0);
}
}
/* ── ETA ── */
.eta {
display: flex;
align-items: center;
gap: 14px;
background: linear-gradient(135deg, var(--teal-d), var(--teal-700));
color: #fff;
border-radius: var(--r-md);
padding: 16px 18px;
}
.eta.is-done {
background: linear-gradient(135deg, #0f7a5a, #0b6149);
}
.eta-ic {
width: 40px;
height: 40px;
flex-shrink: 0;
display: grid;
place-items: center;
border-radius: 12px;
background: rgba(255, 255, 255, 0.18);
font-size: 1.2rem;
}
.eta-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
opacity: 0.82;
font-weight: 600;
}
.eta-value {
font-size: 1.02rem;
font-weight: 700;
margin-top: 2px;
}
/* ── Courier ── */
.courier {
display: flex;
align-items: center;
gap: 12px;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 13px 14px;
}
.courier-avatar {
width: 42px;
height: 42px;
flex-shrink: 0;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--coral);
color: #fff;
font-weight: 700;
font-size: 0.82rem;
}
.courier-info {
flex: 1;
min-width: 0;
}
.courier-name {
font-weight: 600;
font-size: 0.92rem;
}
.courier-meta {
font-size: 0.8rem;
color: var(--muted);
margin-top: 1px;
}
/* ── Items ── */
.section-h {
font-size: 0.92rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.item-list {
list-style: none;
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 2px;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 0;
border-bottom: 1px solid var(--line);
}
.item:last-child {
border-bottom: none;
}
.i-name {
font-weight: 600;
font-size: 0.88rem;
}
.i-qty {
font-size: 0.82rem;
color: var(--muted);
white-space: nowrap;
}
.address {
margin-top: 12px;
display: flex;
align-items: center;
gap: 8px;
background: var(--teal-50);
border-radius: var(--r-sm);
padding: 10px 12px;
font-size: 0.84rem;
font-weight: 500;
color: var(--ink-2);
}
.addr-ic {
color: var(--teal-d);
font-size: 1rem;
}
/* ── Buttons ── */
.btn {
border: none;
border-radius: 11px;
padding: 12px 16px;
font: inherit;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: transform 0.12s, background 0.15s, opacity 0.15s;
}
.btn:active {
transform: translateY(1px);
}
.btn.primary {
background: var(--coral);
color: #fff;
}
.btn.primary:hover {
background: #ff8e7c;
}
.btn.primary:disabled {
background: rgba(47, 158, 111, 0.16);
color: var(--ok);
cursor: default;
}
.btn.ghost {
flex-shrink: 0;
padding: 8px 13px;
font-size: 0.82rem;
background: var(--teal-50);
color: var(--teal-d);
}
.btn.ghost:hover {
background: #d6ece9;
}
/* ── Toast ── */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
background: var(--ink);
color: #fff;
padding: 13px 20px;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-2);
z-index: 50;
max-width: 90vw;
}
@media (max-width: 480px) {
.card {
padding: 20px 18px;
}
.card-head {
flex-direction: column;
}
}
/* Visibility guard: honor the [hidden] attribute over base display */
.courier[hidden] {
display: none;
}// ── Toast ──────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2600);
}
// ── State machine ────────────────────────────────────────────────────────────
// Four fixed stages. Timestamps are illustrative constants (no Date / random).
const STAGES = [
{ label: "Order confirmed", pill: "", time: "2:58 PM" },
{ label: "Preparing", pill: "is-prep", time: "3:12 PM" },
{ label: "Out for delivery", pill: "is-out", time: "3:47 PM" },
{ label: "Delivered", pill: "is-done", time: "4:24 PM" },
];
let current = 0; // index of the active stage
const stepEls = [...document.querySelectorAll(".step")];
const statusPill = document.getElementById("statusPill");
const statusText = document.getElementById("statusText");
const eta = document.getElementById("eta");
const etaLabel = document.getElementById("etaLabel");
const etaValue = document.getElementById("etaValue");
const courier = document.getElementById("courier");
const advanceBtn = document.getElementById("advanceBtn");
function render() {
// Stepper: past = complete (with timestamp), current = highlighted, future = muted.
stepEls.forEach((el, i) => {
const timeEl = el.querySelector(".step-time");
el.classList.toggle("is-complete", i < current);
el.classList.toggle("is-current", i === current);
if (i < current) {
timeEl.textContent = `Completed · ${STAGES[i].time}`;
timeEl.hidden = false;
} else {
timeEl.hidden = true;
}
});
// Status pill.
const stage = STAGES[current];
statusText.textContent = stage.label;
statusPill.className = `status-pill ${stage.pill}`.trim();
// ETA panel.
if (current === STAGES.length - 1) {
eta.classList.add("is-done");
etaLabel.textContent = "Delivered";
etaValue.textContent = `Handed off at ${stage.time}`;
} else {
eta.classList.remove("is-done");
etaLabel.textContent = "Estimated arrival";
etaValue.textContent = "Arriving by 4:30 PM today";
}
// Courier card only while out for delivery.
courier.hidden = current !== 2;
// Advance button locks at the final stage.
if (current === STAGES.length - 1) {
advanceBtn.textContent = "Delivered ✓";
advanceBtn.disabled = true;
}
}
function advance() {
if (current >= STAGES.length - 1) return;
current += 1;
render();
if (current === STAGES.length - 1) {
showToast("Delivered — your prescription has arrived.");
} else {
showToast(`Status updated · ${STAGES[current].label}.`);
}
}
advanceBtn.addEventListener("click", advance);
document.getElementById("contactBtn").addEventListener("click", () => {
showToast("Calling Marco — your courier will pick up shortly.");
});
render();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Delivery Tracker · Northbridge Pharmacy</title>
</head>
<body>
<main class="tracker">
<section class="card" aria-label="Prescription delivery tracker">
<header class="card-head">
<div>
<p class="eyebrow">Northbridge Pharmacy</p>
<h1>Rx #84-2207</h1>
<p class="sub">Standard delivery · placed today</p>
</div>
<span class="status-pill" id="statusPill">
<span class="sp-dot" aria-hidden="true"></span>
<span id="statusText">Order confirmed</span>
</span>
</header>
<ol class="stepper" id="stepper" aria-label="Delivery progress">
<li class="step" data-step="0">
<span class="step-rail" aria-hidden="true"></span>
<span class="step-marker" aria-hidden="true"></span>
<div class="step-body">
<p class="step-title">Order confirmed</p>
<p class="step-desc">We received your prescription.</p>
<p class="step-time" hidden></p>
</div>
</li>
<li class="step" data-step="1">
<span class="step-rail" aria-hidden="true"></span>
<span class="step-marker" aria-hidden="true"></span>
<div class="step-body">
<p class="step-title">Preparing at pharmacy</p>
<p class="step-desc">A pharmacist is filling your order.</p>
<p class="step-time" hidden></p>
</div>
</li>
<li class="step" data-step="2">
<span class="step-rail" aria-hidden="true"></span>
<span class="step-marker" aria-hidden="true"></span>
<div class="step-body">
<p class="step-title">Out for delivery</p>
<p class="step-desc">Your courier is on the way.</p>
<p class="step-time" hidden></p>
</div>
</li>
<li class="step" data-step="3">
<span class="step-rail" aria-hidden="true"></span>
<span class="step-marker" aria-hidden="true"></span>
<div class="step-body">
<p class="step-title">Delivered</p>
<p class="step-desc">Left in hand at the front door.</p>
<p class="step-time" hidden></p>
</div>
</li>
</ol>
<div class="eta" id="eta">
<span class="eta-ic" aria-hidden="true">⏱</span>
<div>
<p class="eta-label" id="etaLabel">Estimated arrival</p>
<p class="eta-value" id="etaValue">Arriving by 4:30 PM today</p>
</div>
</div>
<div class="courier" id="courier" hidden>
<span class="courier-avatar" aria-hidden="true">MD</span>
<div class="courier-info">
<p class="courier-name">Marco Diaz</p>
<p class="courier-meta">Courier · Electric scooter</p>
</div>
<button class="btn ghost" type="button" id="contactBtn">Contact courier</button>
</div>
<section class="items" aria-label="Order items">
<h2 class="section-h">In this delivery</h2>
<ul class="item-list">
<li class="item">
<span class="i-name">Amoxicillin 500mg</span>
<span class="i-qty">×21 capsules</span>
</li>
<li class="item">
<span class="i-name">Ibuprofen 200mg</span>
<span class="i-qty">×30 tablets</span>
</li>
<li class="item">
<span class="i-name">Saline nasal spray</span>
<span class="i-qty">×1 bottle</span>
</li>
</ul>
<p class="address">
<span class="addr-ic" aria-hidden="true">⌖</span>
Deliver to · 14 Maple Row, Apt 3B
</p>
</section>
<button class="btn primary" type="button" id="advanceBtn">Advance status</button>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Pharmacy Delivery Tracker
A parcel-style tracker for prescription medications. The card pairs an order header (Rx number, pharmacy, and a colour-coded status pill) with a vertical four-stage stepper — order confirmed, preparing at pharmacy, out for delivery, delivered — where completed stages show a check and a timestamp, the current stage pulses, and future stages stay muted. An ETA panel announces “Arriving by 4:30 PM” until handoff, a courier card with an initials avatar and a contact button appears only while out for delivery, and the order-items list shows the medications and delivery address. A demo “Advance status” button steps the whole machine forward and locks at delivered.
Illustrative UI only — not intended for real medical use.