Delivery — Schedule a Delivery
A polished schedule-a-delivery flow for a fictional same-day courier. Riders enter pickup and drop-off addresses, choose a package size, pick a date from a seven-day strip and a two-hour time slot, then select Standard or Express service. The estimate recalculates live from base fare, distance, size handling and service multiplier, with a route map preview, animated driver marker, and a confirmation modal issuing a tracking number.
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-sm:0 1px 2px rgba(22,24,29,.06), 0 1px 1px rgba(22,24,29,.04);
--sh-md:0 6px 20px rgba(22,24,29,.08);
--sh-lg:0 18px 50px rgba(22,24,29,.16);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{
font-family:"Inter",system-ui,-apple-system,sans-serif;
line-height:1.5; color:var(--ink); background:var(--bg);
-webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
padding:18px;
}
.app{max-width:1040px;margin:0 auto}
/* Topbar */
.topbar{
display:flex;align-items:center;justify-content:space-between;
background:var(--surface);border:1px solid var(--line);
border-radius:var(--r-lg);padding:14px 18px;box-shadow:var(--sh-sm);margin-bottom:16px;
}
.brand{display:flex;align-items:center;gap:12px}
.brand-mark{
width:40px;height:40px;border-radius:12px;display:grid;place-items:center;
background:linear-gradient(140deg,var(--brand),var(--brand-d));color:#fff;
box-shadow:0 4px 12px rgba(255,90,44,.4);
}
.brand strong{display:block;font-weight:800;font-size:1.02rem}
.brand small{display:block;color:var(--muted);font-size:.78rem}
.steps-mini{display:flex;gap:7px}
.steps-mini .dot{width:9px;height:9px;border-radius:50%;background:var(--line)}
.steps-mini .dot.on{background:var(--brand)}
/* Layout */
.layout{display:grid;grid-template-columns:1fr 340px;gap:16px;align-items:start}
.panel{
background:var(--surface);border:1px solid var(--line);
border-radius:var(--r-lg);box-shadow:var(--sh-sm);
}
.form{padding:6px 22px 22px}
.summary{padding:20px 20px 22px;position:sticky;top:18px}
.block{padding:18px 0;border-bottom:1px solid var(--line)}
.block:last-child{border-bottom:none}
.block-title{
font-size:.72rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
color:var(--muted);margin-bottom:12px;
}
/* Fields */
.field{margin-bottom:12px}
.field label{display:block;font-size:.82rem;font-weight:600;color:var(--ink-2);margin-bottom:5px}
.input-wrap{position:relative}
.input-wrap input{
width:100%;font:inherit;font-size:.95rem;color:var(--ink);
padding:11px 12px 11px 38px;border:1px solid var(--line);
border-radius:var(--r-md);background:var(--bg);transition:border-color .15s,box-shadow .15s,background .15s;
}
.input-wrap input:focus{outline:none;border-color:var(--brand);background:#fff;box-shadow:0 0 0 3px rgba(255,90,44,.16)}
.input-wrap.pin::before{
content:"";position:absolute;left:14px;top:50%;width:10px;height:10px;
border-radius:50%;transform:translateY(-50%);
}
.input-wrap.pickup::before{background:var(--ok);box-shadow:0 0 0 3px rgba(31,157,98,.18)}
.input-wrap.drop::before{background:var(--brand);box-shadow:0 0 0 3px rgba(255,90,44,.18)}
/* Route card */
.route-card{margin-top:6px;border:1px solid var(--line);border-radius:var(--r-md);overflow:hidden;background:var(--bg)}
.route-map{
height:120px;position:relative;
background:
linear-gradient(rgba(91,141,239,.07) 1px,transparent 1px) 0 0/100% 24px,
linear-gradient(90deg,rgba(91,141,239,.07) 1px,transparent 1px) 0 0/24px 100%,
#eef2fb;
}
.route-svg{position:absolute;inset:0;width:100%;height:100%}
.route-line{fill:none;stroke:var(--track);stroke-width:4;stroke-linecap:round;stroke-dasharray:7 7;opacity:.85;
animation:dash 1.4s linear infinite}
@keyframes dash{to{stroke-dashoffset:-14}}
.node{stroke:#fff;stroke-width:3}
.node.start{fill:var(--ok)}
.node.end{fill:var(--brand)}
.driver{fill:var(--brand);stroke:#fff;stroke-width:3;filter:drop-shadow(0 2px 4px rgba(255,90,44,.5))}
.route-meta{display:flex;gap:8px;padding:10px 12px;background:var(--surface);border-top:1px solid var(--line)}
.route-meta span{flex:1;display:flex;flex-direction:column}
.route-meta strong{font-size:.95rem;font-weight:700}
.route-meta small{color:var(--muted);font-size:.72rem}
/* Size grid */
.size-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.size{
display:flex;flex-direction:column;gap:3px;text-align:left;cursor:pointer;
padding:12px;border:1.5px solid var(--line);border-radius:var(--r-md);
background:var(--surface);font:inherit;color:var(--ink);transition:.15s;
}
.size:hover{border-color:var(--ink-2)}
.size .size-ico{font-size:1.3rem;line-height:1}
.size strong{font-size:.9rem;font-weight:700}
.size small{font-size:.7rem;color:var(--muted);line-height:1.35}
.size[aria-checked="true"]{border-color:var(--brand);background:rgba(255,90,44,.06);box-shadow:inset 0 0 0 1px var(--brand)}
.size:focus-visible,.service:focus-visible,.slot:focus-visible,.day:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
/* Date row */
.date-row{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px;-webkit-overflow-scrolling:touch}
.day{
flex:0 0 auto;min-width:62px;text-align:center;cursor:pointer;font:inherit;color:var(--ink);
padding:9px 6px;border:1.5px solid var(--line);border-radius:var(--r-md);background:var(--surface);transition:.15s;
}
.day:hover{border-color:var(--ink-2)}
.day .dow{display:block;font-size:.66rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.day .dnum{display:block;font-size:1.15rem;font-weight:800;line-height:1.2}
.day .mon{display:block;font-size:.64rem;color:var(--muted)}
.day[aria-checked="true"]{border-color:var(--brand);background:var(--brand);box-shadow:0 4px 12px rgba(255,90,44,.35)}
.day[aria-checked="true"] .dow,.day[aria-checked="true"] .mon{color:rgba(255,255,255,.85)}
.day[aria-checked="true"] .dnum{color:#fff}
/* Slot grid */
.slot-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:9px}
.slot{
display:flex;align-items:center;justify-content:space-between;cursor:pointer;font:inherit;color:var(--ink);
padding:11px 13px;border:1.5px solid var(--line);border-radius:var(--r-md);background:var(--surface);transition:.15s;
}
.slot:hover:not([disabled]){border-color:var(--ink-2)}
.slot .slot-time{font-size:.88rem;font-weight:600}
.slot .slot-tag{font-size:.68rem;font-weight:700;color:var(--muted)}
.slot[aria-checked="true"]{border-color:var(--brand);background:rgba(255,90,44,.06);box-shadow:inset 0 0 0 1px var(--brand)}
.slot[aria-checked="true"] .slot-tag{color:var(--brand)}
.slot[disabled]{opacity:.45;cursor:not-allowed}
.slot[disabled] .slot-tag{color:var(--danger)}
/* Service */
.service-grid{display:grid;gap:10px}
.service{
display:grid;gap:5px;text-align:left;cursor:pointer;font:inherit;color:var(--ink);
padding:13px 15px;border:1.5px solid var(--line);border-radius:var(--r-md);background:var(--surface);transition:.15s;
}
.service:hover{border-color:var(--ink-2)}
.svc-top{display:flex;align-items:center;gap:8px}
.svc-top strong{font-size:.96rem;font-weight:700}
.service small{font-size:.76rem;color:var(--muted)}
.svc-price{font-size:.82rem;font-weight:700;color:var(--ink-2)}
.service[aria-checked="true"]{border-color:var(--brand);background:rgba(255,90,44,.06);box-shadow:inset 0 0 0 1px var(--brand)}
.pill{font-size:.64rem;font-weight:700;padding:2px 8px;border-radius:999px;letter-spacing:.03em}
.pill.ok{background:rgba(31,157,98,.14);color:var(--ok)}
.pill.warn{background:rgba(232,148,34,.16);color:var(--warn)}
/* Summary */
.price-hero{display:flex;align-items:flex-start;gap:2px;color:var(--ink);margin-top:4px}
.price-hero .currency{font-size:1.3rem;font-weight:700;margin-top:6px;color:var(--ink-2)}
.price-num{font-size:2.9rem;font-weight:800;line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.price-note{font-size:.8rem;color:var(--muted);margin:4px 0 14px;font-weight:500}
.break{list-style:none;display:grid;gap:7px;margin-bottom:14px}
.break li{display:flex;justify-content:space-between;font-size:.83rem;color:var(--ink-2)}
.break li span:last-child{font-weight:600;font-variant-numeric:tabular-nums}
.break li.discount span:last-child{color:var(--ok)}
.break li.total{border-top:1px dashed var(--line);padding-top:8px;margin-top:1px;font-weight:700;color:var(--ink);font-size:.92rem}
.recap{display:grid;gap:8px;padding:12px;background:var(--bg);border-radius:var(--r-md);margin-bottom:16px}
.recap div{display:flex;justify-content:space-between;gap:12px}
.recap dt{font-size:.74rem;color:var(--muted);font-weight:600}
.recap dd{font-size:.79rem;font-weight:600;text-align:right;max-width:62%}
.confirm{
width:100%;font:inherit;font-size:.95rem;font-weight:700;color:#fff;cursor:pointer;
padding:13px;border:none;border-radius:var(--r-md);
background:linear-gradient(140deg,var(--brand),var(--brand-d));box-shadow:0 6px 16px rgba(255,90,44,.35);
transition:transform .12s,box-shadow .12s,opacity .15s;
}
.confirm:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 10px 22px rgba(255,90,44,.42)}
.confirm:active:not(:disabled){transform:translateY(0)}
.confirm:disabled{background:#cbcdd3;box-shadow:none;cursor:not-allowed}
.fine{font-size:.7rem;color:var(--muted);text-align:center;margin-top:10px;line-height:1.45}
/* Overlay */
.overlay{position:fixed;inset:0;background:rgba(22,24,29,.5);backdrop-filter:blur(3px);
display:grid;place-items:center;padding:20px;z-index:50;animation:fade .2s ease}
.overlay[hidden]{display:none}
@keyframes fade{from{opacity:0}to{opacity:1}}
.modal{background:var(--surface);border-radius:var(--r-lg);box-shadow:var(--sh-lg);
padding:30px 26px 26px;max-width:340px;width:100%;text-align:center;animation:pop .26s cubic-bezier(.2,.9,.3,1.2)}
@keyframes pop{from{opacity:0;transform:translateY(12px) scale(.96)}to{opacity:1;transform:none}}
.check{display:grid;place-items:center;margin-bottom:8px}
.ck-c{fill:none;stroke:var(--ok);stroke-width:3;stroke-dasharray:151;stroke-dashoffset:151;animation:ck-c .5s ease forwards}
.ck-p{fill:none;stroke:var(--ok);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:40;stroke-dashoffset:40;animation:ck-p .35s .45s ease forwards}
@keyframes ck-c{to{stroke-dashoffset:0}}
@keyframes ck-p{to{stroke-dashoffset:0}}
.modal h3{font-size:1.25rem;font-weight:800;margin-bottom:6px}
.ok-sub{font-size:.85rem;color:var(--muted);margin-bottom:14px;line-height:1.5}
.ok-id{font-size:.8rem;color:var(--ink-2);background:var(--bg);border-radius:var(--r-sm);padding:8px;margin-bottom:18px}
.ok-id strong{font-weight:800;letter-spacing:.03em}
/* Toast */
.toast-host{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);display:grid;gap:8px;z-index:60;width:max-content;max-width:90vw}
.toast{background:var(--ink);color:#fff;font-size:.84rem;font-weight:500;padding:11px 16px;border-radius:var(--r-md);
box-shadow:var(--sh-md);animation:t-in .25s ease, t-out .3s ease 2.4s forwards}
@keyframes t-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes t-out{to{opacity:0;transform:translateY(10px)}}
/* Responsive */
@media (max-width:840px){
.layout{grid-template-columns:1fr}
.summary{position:static}
}
@media (max-width:520px){
body{padding:12px}
.size-grid{grid-template-columns:1fr;gap:8px}
.size{flex-direction:row;align-items:center;gap:10px}
.size .size-ico{font-size:1.1rem}
.size small{flex:1}
.slot-grid{grid-template-columns:1fr}
.price-num{font-size:2.5rem}
.brand small{display:none}
}
@media (prefers-reduced-motion:reduce){
*{animation-duration:.001s!important;animation-iteration-count:1!important}
}"use strict";
/* ---------- Pricing model (fictional) ---------- */
const BASE = 4.0; // base dispatch fare
const PER_MILE = 0.95;
const SIZE_FEE = { small: 0, medium: 4.5, large: 11 };
const SIZE_LABEL = { small: "Small", medium: "Medium", large: "Large" };
const SERVICE = {
standard: { mult: 1, label: "Standard", note: "Delivered within your slot" },
express: { mult: 1.55, label: "Express", note: "First in the driver queue" },
};
const DISTANCE_MI = 6.2;
/* ---------- State ---------- */
const state = {
size: "small",
service: "standard",
date: null, // {dow, dnum, mon, full}
slot: null, // {time, tag}
};
/* ---------- Helpers ---------- */
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
const money = (n) => `$${n.toFixed(2)}`;
function toast(msg) {
const host = $("#toastHost");
const el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
host.appendChild(el);
setTimeout(() => el.remove(), 2900);
}
/* radiogroup: single-select, set aria-checked + active class */
function selectInGroup(group, target) {
$$('[role="radio"]', group).forEach((b) => b.setAttribute("aria-checked", "false"));
target.setAttribute("aria-checked", "true");
}
/* ---------- Build date strip (next 7 days) ---------- */
function buildDates() {
const row = $("#dateRow");
const today = new Date();
const DOW = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const MON = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
for (let i = 0; i < 7; i++) {
const d = new Date(today);
d.setDate(today.getDate() + i);
const btn = document.createElement("button");
btn.type = "button";
btn.className = "day";
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", "false");
const label = i === 0 ? "Today" : i === 1 ? "Tmrw" : DOW[d.getDay()];
btn.innerHTML =
`<span class="dow">${label}</span>` +
`<span class="dnum">${d.getDate()}</span>` +
`<span class="mon">${MON[d.getMonth()]}</span>`;
const info = {
dow: label,
dnum: d.getDate(),
mon: MON[d.getMonth()],
full: `${i === 0 ? "Today" : i === 1 ? "Tomorrow" : DOW[d.getDay()]}, ${MON[d.getMonth()]} ${d.getDate()}`,
index: i,
};
btn.addEventListener("click", () => {
selectInGroup(row, btn);
state.date = info;
buildSlots(i === 0);
updateRecap();
validate();
});
row.appendChild(btn);
}
// preselect today
const first = $(".day", row);
first.setAttribute("aria-checked", "true");
state.date = {
dow: "Today",
dnum: today.getDate(),
mon: MON[today.getMonth()],
full: `Today, ${MON[today.getMonth()]} ${today.getDate()}`,
index: 0,
};
}
/* ---------- Build time slots (some sold out if "today") ---------- */
const SLOTS = [
{ time: "9:00–11:00", tag: "Morning" },
{ time: "11:00–1:00", tag: "Midday" },
{ time: "1:00–3:00", tag: "Afternoon" },
{ time: "3:00–5:00", tag: "Late PM" },
{ time: "5:00–7:00", tag: "Evening" },
{ time: "7:00–9:00", tag: "Night" },
];
function buildSlots(isToday) {
const grid = $("#slotGrid");
grid.innerHTML = "";
// For "today", earlier slots are sold out
const soldOutUntil = isToday ? 2 : 0;
SLOTS.forEach((s, i) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "slot";
btn.setAttribute("role", "radio");
btn.setAttribute("aria-checked", "false");
const soldOut = i < soldOutUntil;
if (soldOut) btn.disabled = true;
btn.innerHTML =
`<span class="slot-time">${s.time}</span>` +
`<span class="slot-tag">${soldOut ? "Sold out" : s.tag}</span>`;
if (!soldOut) {
btn.addEventListener("click", () => {
selectInGroup(grid, btn);
state.slot = s;
updateRecap();
validate();
});
}
grid.appendChild(btn);
});
// reset slot selection when rebuilding
state.slot = null;
updateRecap();
validate();
}
/* ---------- Pricing ---------- */
function computeBreakdown(serviceKey, sizeKey) {
const svc = SERVICE[serviceKey];
const base = BASE;
const distance = PER_MILE * DISTANCE_MI;
const sizeFee = SIZE_FEE[sizeKey];
const subtotal = base + distance + sizeFee;
const total = subtotal * svc.mult;
const expressAdd = total - subtotal;
return { base, distance, sizeFee, subtotal, total, expressAdd };
}
function priceFor(serviceKey) {
return computeBreakdown(serviceKey, state.size).total;
}
function renderServicePrices() {
$$(".svc-price[data-base]").forEach((el) => {
const key = el.getAttribute("data-base");
el.textContent = `from ${money(priceFor(key))}`;
});
}
function updatePrice() {
const b = computeBreakdown(state.service, state.size);
$("#priceNum").textContent = b.total.toFixed(2);
$("#priceNote").textContent =
`${SERVICE[state.service].label} · ${SIZE_LABEL[state.size]} package`;
const bd = $("#breakdown");
bd.innerHTML = "";
const rows = [
["Base dispatch", money(b.base)],
[`Distance · ${DISTANCE_MI} mi`, money(b.distance)],
];
if (b.sizeFee > 0) rows.push([`${SIZE_LABEL[state.size]} handling`, money(b.sizeFee)]);
rows.forEach(([k, v]) => addRow(bd, k, v));
if (state.service === "express") {
addRow(bd, "Express priority", `+${money(b.expressAdd)}`, "discount");
}
addRow(bd, "Total estimate", money(b.total), "total");
renderServicePrices();
}
function addRow(ul, label, val, cls) {
const li = document.createElement("li");
if (cls) li.className = cls;
li.innerHTML = `<span>${label}</span><span>${val}</span>`;
ul.appendChild(li);
}
/* ---------- Recap ---------- */
function shortAddr(v) {
return v.split(",")[0].trim() || "—";
}
function updateRecap() {
$("#recapFrom").textContent = shortAddr($("#pickup").value);
$("#recapTo").textContent = shortAddr($("#dropoff").value);
if (state.date && state.slot) {
$("#recapWhen").textContent = `${state.date.full} · ${state.slot.time}`;
} else if (state.date) {
$("#recapWhen").textContent = `${state.date.full} · select a slot`;
} else {
$("#recapWhen").textContent = "Select a slot";
}
}
/* ---------- Validation ---------- */
function validate() {
const ok =
$("#pickup").value.trim() &&
$("#dropoff").value.trim() &&
state.date &&
state.slot;
$("#confirmBtn").disabled = !ok;
return ok;
}
/* ---------- Wire static groups ---------- */
function wireGroups() {
const sizeGroup = $(".size-grid");
$$(".size", sizeGroup).forEach((btn) => {
btn.addEventListener("click", () => {
selectInGroup(sizeGroup, btn);
state.size = btn.dataset.size;
updatePrice();
});
});
const svcGroup = $(".service-grid");
$$(".service", svcGroup).forEach((btn) => {
btn.addEventListener("click", () => {
selectInGroup(svcGroup, btn);
state.service = btn.dataset.service;
updatePrice();
toast(
state.service === "express"
? "Express selected — priority dispatch"
: "Standard service selected"
);
});
});
// keyboard: Enter/Space activates focused radio
$$('[role="radio"]').forEach((el) => {
el.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
el.click();
}
});
});
$("#pickup").addEventListener("input", () => {
updateRecap();
validate();
});
$("#dropoff").addEventListener("input", () => {
updateRecap();
validate();
});
}
/* ---------- Submit / success ---------- */
function makeId() {
let s = "PCX-";
for (let i = 0; i < 6; i++) s += Math.floor(Math.random() * 10);
return s;
}
function wireSubmit() {
$("#bookForm").addEventListener("submit", (e) => {
e.preventDefault();
if (!validate()) {
toast("Pick a date and time slot first");
return;
}
const total = priceFor(state.service);
const id = makeId();
$("#okId").textContent = id;
$("#okSub").textContent =
`${SERVICE[state.service].label} · ${SIZE_LABEL[state.size]} · ${state.date.full}, ${state.slot.time}. ` +
`Estimated ${money(total)}.`;
$("#overlay").hidden = false;
// advance the mini step dots
$$(".steps-mini .dot").forEach((d) => d.classList.add("on"));
});
$("#okClose").addEventListener("click", () => {
$("#overlay").hidden = true;
toast("Tracking link sent to your phone");
});
$("#overlay").addEventListener("click", (e) => {
if (e.target === $("#overlay")) $("#overlay").hidden = true;
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && !$("#overlay").hidden) $("#overlay").hidden = true;
});
}
/* ---------- Init ---------- */
buildDates();
buildSlots(true); // today by default → early slots sold out
wireGroups();
wireSubmit();
updatePrice();
updateRecap();
validate();
renderServicePrices();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pelican Courier — Schedule a Delivery</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="app">
<header class="topbar">
<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 7h11v8H3z"/><path d="M14 10h4l3 3v2h-7z"/><circle cx="7" cy="18" r="1.8"/><circle cx="17" cy="18" r="1.8"/>
</svg>
</span>
<div>
<strong>Pelican Courier</strong>
<small>Same-day & scheduled delivery</small>
</div>
</div>
<div class="steps-mini" aria-hidden="true">
<span class="dot on"></span><span class="dot"></span><span class="dot"></span>
</div>
</header>
<main class="layout">
<form class="panel form" id="bookForm" novalidate aria-label="Schedule a delivery">
<section class="block">
<h2 class="block-title">Route</h2>
<div class="field">
<label for="pickup">Pickup address</label>
<div class="input-wrap pin pickup">
<input id="pickup" name="pickup" type="text" autocomplete="off"
value="221 Harbor Walk, Westport" required />
</div>
</div>
<div class="field">
<label for="dropoff">Drop-off address</label>
<div class="input-wrap pin drop">
<input id="dropoff" name="dropoff" type="text" autocomplete="off"
value="14 Maple Court, Eastside" required />
</div>
</div>
<div class="route-card" aria-label="Estimated route">
<div class="route-map" role="img" aria-label="Map preview of delivery route">
<svg class="route-svg" viewBox="0 0 300 120" preserveAspectRatio="none">
<path class="route-line" d="M30 92 C 90 92, 110 30, 170 30 S 250 40, 272 28" />
<circle class="node start" cx="30" cy="92" r="6" />
<circle class="node end" cx="272" cy="28" r="6" />
<circle class="driver" cx="30" cy="92" r="7">
<animateMotion dur="6s" repeatCount="indefinite"
path="M0 0 C 60 0, 80 -62, 140 -62 S 220 -52, 242 -64" />
</circle>
</svg>
</div>
<div class="route-meta">
<span><strong id="distance">6.2 mi</strong><small>distance</small></span>
<span><strong id="driveTime">~22 min</strong><small>drive</small></span>
</div>
</div>
</section>
<section class="block">
<h2 class="block-title">Package size</h2>
<div class="size-grid" role="radiogroup" aria-label="Package size">
<button type="button" class="size" role="radio" aria-checked="true" data-size="small">
<span class="size-ico">📦</span>
<strong>Small</strong>
<small>Up to 5 lb · envelope/box</small>
</button>
<button type="button" class="size" role="radio" aria-checked="false" data-size="medium">
<span class="size-ico">🧰</span>
<strong>Medium</strong>
<small>5–25 lb · carry-on</small>
</button>
<button type="button" class="size" role="radio" aria-checked="false" data-size="large">
<span class="size-ico">🛋️</span>
<strong>Large</strong>
<small>25–60 lb · bulky</small>
</button>
</div>
</section>
<section class="block">
<h2 class="block-title">Date</h2>
<div class="date-row" id="dateRow" role="radiogroup" aria-label="Delivery date"></div>
</section>
<section class="block">
<h2 class="block-title">Time slot</h2>
<div class="slot-grid" id="slotGrid" role="radiogroup" aria-label="Time slot"></div>
</section>
<section class="block">
<h2 class="block-title">Service level</h2>
<div class="service-grid" role="radiogroup" aria-label="Service level">
<button type="button" class="service" role="radio" aria-checked="true" data-service="standard">
<span class="svc-top"><strong>Standard</strong><span class="pill ok">Eco</span></span>
<small>Delivered within your slot</small>
<span class="svc-price" data-base="standard"></span>
</button>
<button type="button" class="service" role="radio" aria-checked="false" data-service="express">
<span class="svc-top"><strong>Express</strong><span class="pill warn">Priority</span></span>
<small>First in the driver queue · ~45 min faster</small>
<span class="svc-price" data-base="express"></span>
</button>
</div>
</section>
</form>
<aside class="panel summary" aria-label="Order summary">
<h2 class="block-title">Estimate</h2>
<div class="price-hero">
<span class="currency">$</span>
<span class="price-num" id="priceNum">0.00</span>
</div>
<p class="price-note" id="priceNote">Standard · Small package</p>
<ul class="break" id="breakdown"></ul>
<dl class="recap">
<div><dt>From</dt><dd id="recapFrom">—</dd></div>
<div><dt>To</dt><dd id="recapTo">—</dd></div>
<div><dt>When</dt><dd id="recapWhen">Select a slot</dd></div>
</dl>
<button type="submit" form="bookForm" class="confirm" id="confirmBtn" disabled>
Confirm delivery
</button>
<p class="fine">No charge until a driver accepts. Free cancellation up to pickup.</p>
</aside>
</main>
</div>
<!-- Success overlay -->
<div class="overlay" id="overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="okTitle">
<div class="check" aria-hidden="true">
<svg viewBox="0 0 52 52" width="56" height="56"><circle class="ck-c" cx="26" cy="26" r="24"/><path class="ck-p" d="M16 27l7 7 14-15"/></svg>
</div>
<h3 id="okTitle">Delivery scheduled</h3>
<p class="ok-sub" id="okSub"></p>
<div class="ok-id">Tracking <strong id="okId">PCX-000000</strong></div>
<button type="button" class="confirm" id="okClose">Done</button>
</div>
</div>
<div class="toast-host" id="toastHost" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Schedule a Delivery
A complete booking flow for Pelican Courier, a fictional same-day delivery service. The left panel walks through the order: pickup and drop-off address fields with colored map pins, a package-size selector (Small / Medium / Large), a horizontally scrolling seven-day date strip, a two-hour time-slot grid, and a Standard vs. Express service choice. A compact route card previews the trip with an animated dashed route line and a driver marker gliding from origin to destination.
The right-hand summary panel is the live price engine. Every interaction
recomputes the estimate from a transparent breakdown — base dispatch, distance,
size handling, and an Express priority multiplier — and updates the big hero
price, the line-item list, and the order recap. Earlier slots are marked sold
out when the chosen day is today, the confirm button stays disabled until the
booking is complete, and submitting opens a success modal with an animated check
and a generated PCX- tracking number.
Everything is vanilla HTML, CSS, and JavaScript: radiogroup semantics with
keyboard activation, a small toast() helper for feedback, reduced-motion
support, and a layout that collapses to a mobile-first single column down to
360px.
Illustrative UI only — fictional brand, not a real delivery service.