Map Delivery Tracking
Original · freedelivery tracking with route
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/map-delivery-tracking.json"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type Stop = {
id: string;
t: number;
title: string;
detail: string;
};
type Sample = { x: number; y: number };
type Point = { x: number; y: number; angle: number };
const ROUTE_D =
"M126 370 L126 296 Q126 280 142 280 L222 280 Q238 280 238 264 L238 206 Q238 190 254 190 L334 190 Q350 190 350 174 L350 116 Q350 100 366 100 L574 100";
const PICKUP = { x: 126, y: 370 };
const DROPOFF = { x: 574, y: 100 };
const START_MIN = 8 * 60 + 41;
const TRIP_MIN = 91;
const TRIP_KM = 6.4;
const BLOCK_X = [24, 136, 248, 360, 472];
const BLOCK_Y = [20, 110, 200, 290];
const PARK = { x: 360, y: 110 };
const BLOCKS: Array<{ x: number; y: number }> = BLOCK_X.flatMap((x) =>
BLOCK_Y.filter((y) => !(x === PARK.x && y === PARK.y)).map((y) => ({ x, y })),
);
const STOPS: Stop[] = [
{ id: "picked", t: 0, title: "Picked up", detail: "Kembali Roastery — 12 Jln Sulaiman" },
{ id: "depot", t: 0.32, title: "Scanned at North Hub", detail: "Bay 4 — sorted to bike route 17" },
{ id: "out", t: 0.5, title: "Out for delivery", detail: "Priya N. took the parcel at Bay 4" },
{ id: "near", t: 0.86, title: "Three stops away", detail: "Turning onto Arcadia Boulevard" },
{ id: "done", t: 1, title: "Delivered", detail: "Unit 14-02, Arcadia Tower — hand to resident" },
];
function sampleAt(samples: Sample[], u: number): Sample {
if (samples.length === 0) return PICKUP;
const f = Math.min(1, Math.max(0, u)) * (samples.length - 1);
const i = Math.floor(f);
const j = Math.min(samples.length - 1, i + 1);
const k = f - i;
return {
x: samples[i].x + (samples[j].x - samples[i].x) * k,
y: samples[i].y + (samples[j].y - samples[i].y) * k,
};
}
function fmtTime(totalMin: number): string {
const m = Math.round(totalMin);
const h = Math.floor(m / 60) % 24;
const mm = ((m % 60) + 60) % 60;
return `${String(h).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
}
export default function MapDeliveryTracking() {
const reduce = useReducedMotion();
const pathRef = useRef<SVGPathElement | null>(null);
const startFromRef = useRef(0);
const [len, setLen] = useState(0);
const [samples, setSamples] = useState<Sample[]>([]);
const [progress, setProgress] = useState(0.62);
const [playing, setPlaying] = useState(false);
const [notify, setNotify] = useState(true);
useEffect(() => {
const p = pathRef.current;
if (!p) return;
const total = p.getTotalLength();
const steps = 360;
const next: Sample[] = [];
for (let i = 0; i <= steps; i += 1) {
const pt = p.getPointAtLength((i / steps) * total);
next.push({ x: pt.x, y: pt.y });
}
setLen(total);
setSamples(next);
}, []);
useEffect(() => {
if (!playing) return;
let raf = 0;
const from = startFromRef.current;
const t0 = performance.now();
const tick = (now: number) => {
const p = Math.min(1, from + (now - t0) / 14000);
setProgress(p);
if (p >= 1) {
setPlaying(false);
return;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [playing]);
const togglePlay = () => {
if (playing) {
setPlaying(false);
return;
}
if (reduce) {
setProgress(1);
return;
}
const from = progress >= 1 ? 0 : progress;
startFromRef.current = from;
setProgress(from);
setPlaying(true);
};
const courier = useMemo<Point>(() => {
if (samples.length === 0) return { x: PICKUP.x, y: PICKUP.y, angle: -90 };
const at = sampleAt(samples, progress);
const behind = sampleAt(samples, progress - 0.005);
const ahead = sampleAt(samples, progress + 0.005);
const angle = (Math.atan2(ahead.y - behind.y, ahead.x - behind.x) * 180) / Math.PI;
return { x: at.x, y: at.y, angle };
}, [progress, samples]);
const stopPoints = useMemo(() => {
if (samples.length === 0) return null;
return STOPS.map((s) => {
const pt = sampleAt(samples, s.t);
return { id: s.id, t: s.t, x: pt.x, y: pt.y };
});
}, [samples]);
const reached = STOPS.filter((s) => s.t <= progress + 0.001);
const current = reached.length > 0 ? reached[reached.length - 1] : STOPS[0];
const minsAway = Math.max(0, Math.round((1 - progress) * TRIP_MIN));
const kmAway = (1 - progress) * TRIP_KM;
const pct = Math.round(progress * 100);
const arrival = fmtTime(START_MIN + TRIP_MIN);
const delivered = progress >= 1;
const focusRing =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";
return (
<section className="relative w-full bg-slate-50 px-4 py-20 text-slate-900 sm:px-6 sm:py-24 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes mdt-ping {
0% { transform: scale(0.45); opacity: 0.7; }
70%, 100% { transform: scale(1.75); opacity: 0; }
}
@keyframes mdt-dash {
to { stroke-dashoffset: -22; }
}
@keyframes mdt-glow {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.35; transform: scale(0.72); }
}
.mdt-ping {
transform-box: fill-box;
transform-origin: center;
animation: mdt-ping 2.4s cubic-bezier(0, 0, 0.2, 1) infinite;
}
.mdt-flow {
animation: mdt-dash 1.1s linear infinite;
}
.mdt-glow {
animation: mdt-glow 1.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.mdt-ping, .mdt-flow, .mdt-glow { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-6xl">
<motion.header
initial={reduce ? false : { opacity: 0, y: 14 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="mb-8 max-w-2xl sm:mb-10"
>
<span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium tracking-wide text-indigo-700 dark:border-indigo-900 dark:bg-indigo-950/60 dark:text-indigo-300">
<span className="mdt-glow h-1.5 w-1.5 rounded-full bg-indigo-500" />
Live tracking
</span>
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Order KR-4821 is on the way
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Two bags of Sumatra Lintong and a hand grinder, riding across town from the roastery to
Arcadia Tower. Scrub the route to see where the courier has been.
</p>
</motion.header>
<div className="grid gap-6 lg:grid-cols-[1.55fr_1fr]">
<motion.div
initial={reduce ? false : { opacity: 0, y: 18 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.55, ease: "easeOut" }}
className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm lg:self-start dark:border-slate-800 dark:bg-slate-900/50"
>
<div className="relative">
<svg
viewBox="0 0 600 400"
role="img"
aria-label="Street map of the delivery route from Kembali Roastery to Arcadia Tower, with the courier's position marked along the way."
className="block h-auto w-full"
>
<rect x="0" y="0" width="600" height="400" className="fill-white dark:fill-slate-900" />
{BLOCKS.map((b) => (
<rect
key={`${b.x}-${b.y}`}
x={b.x}
y={b.y}
width="92"
height="70"
rx="4"
className="fill-slate-100 dark:fill-slate-800/80"
/>
))}
<rect
x={PARK.x}
y={PARK.y}
width="92"
height="70"
rx="4"
className="fill-emerald-100 dark:fill-emerald-950"
/>
<circle cx="406" cy="145" r="14" className="fill-emerald-200/70 dark:fill-emerald-900/70" />
<path
d="M600 232 C 548 264, 524 320, 476 400 L600 400 Z"
className="fill-sky-100 dark:fill-sky-950"
/>
<g className="fill-slate-400 dark:fill-slate-500" fontSize="8" fontWeight="600" letterSpacing="0.6">
<text x="70" y="318" textAnchor="middle">KEMBALI</text>
<text x="70" y="328" textAnchor="middle">ROASTERY</text>
<text x="294" y="232" textAnchor="middle">NORTH HUB</text>
<text x="294" y="242" textAnchor="middle">BAY 4</text>
<text x="406" y="180" textAnchor="middle">SELAT PARK</text>
<text x="518" y="52" textAnchor="middle">ARCADIA</text>
<text x="518" y="62" textAnchor="middle">TOWER</text>
<text x="546" y="330" textAnchor="middle" transform="rotate(58 546 330)">
KELANA RIVER
</text>
</g>
<path
ref={pathRef}
d={ROUTE_D}
fill="none"
strokeWidth="7"
strokeLinecap="round"
strokeLinejoin="round"
className="stroke-slate-200 dark:stroke-slate-700"
/>
<path
d={ROUTE_D}
fill="none"
strokeWidth="3"
strokeLinecap="round"
strokeDasharray="2 9"
className="mdt-flow stroke-slate-400 dark:stroke-slate-500"
/>
{len > 0 && (
<>
<path
d={ROUTE_D}
fill="none"
strokeWidth="13"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray={len}
strokeDashoffset={len * (1 - progress)}
className="stroke-indigo-500/15 dark:stroke-indigo-400/15"
/>
<path
d={ROUTE_D}
fill="none"
strokeWidth="5"
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray={len}
strokeDashoffset={len * (1 - progress)}
className="stroke-indigo-500 dark:stroke-indigo-400"
/>
</>
)}
{stopPoints
?.filter((s) => s.t > 0 && s.t < 1)
.map((s) => (
<circle
key={s.id}
cx={s.x}
cy={s.y}
r="4.5"
strokeWidth="2.5"
className={
s.t <= progress + 0.001
? "fill-white stroke-indigo-500 dark:fill-slate-900 dark:stroke-indigo-400"
: "fill-white stroke-slate-300 dark:fill-slate-900 dark:stroke-slate-600"
}
/>
))}
<g transform={`translate(${PICKUP.x} ${PICKUP.y})`}>
<ellipse cx="0" cy="1" rx="7" ry="2.5" className="fill-slate-900/15 dark:fill-black/40" />
<path
d="M0 0 c-6 -8 -9.5 -12.5 -9.5 -17.5 a9.5 9.5 0 1 1 19 0 c0 5 -3.5 9.5 -9.5 17.5z"
className="fill-emerald-500 stroke-white dark:stroke-slate-900"
strokeWidth="1.5"
/>
<circle cx="0" cy="-17.5" r="3.6" className="fill-white dark:fill-slate-900" />
</g>
<g transform={`translate(${DROPOFF.x} ${DROPOFF.y})`}>
<ellipse cx="0" cy="1" rx="7" ry="2.5" className="fill-slate-900/15 dark:fill-black/40" />
<path
d="M0 0 c-6 -8 -9.5 -12.5 -9.5 -17.5 a9.5 9.5 0 1 1 19 0 c0 5 -3.5 9.5 -9.5 17.5z"
className={
delivered
? "fill-emerald-500 stroke-white dark:stroke-slate-900"
: "fill-rose-500 stroke-white dark:stroke-slate-900"
}
strokeWidth="1.5"
/>
<circle cx="0" cy="-17.5" r="3.6" className="fill-white dark:fill-slate-900" />
</g>
<g transform={`translate(${courier.x} ${courier.y})`}>
<circle r="10" className="mdt-ping fill-indigo-500/40 dark:fill-indigo-400/40" />
<circle
r="10.5"
strokeWidth="2.5"
className="fill-white stroke-indigo-600 dark:fill-slate-900 dark:stroke-indigo-400"
/>
<g transform={`rotate(${courier.angle})`}>
<path
d="M-3.4 -4.4 L5 0 L-3.4 4.4 L-1.4 0 Z"
className="fill-indigo-600 dark:fill-indigo-400"
/>
</g>
</g>
</svg>
<div className="pointer-events-none absolute left-3 top-3 flex items-center gap-2.5 rounded-xl border border-slate-200 bg-white/90 px-2.5 py-2 shadow-sm backdrop-blur-sm sm:left-4 sm:top-4 dark:border-slate-700 dark:bg-slate-900/90">
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-indigo-600 text-xs font-semibold text-white">
PN
</span>
<span className="leading-tight">
<span className="block text-xs font-semibold text-slate-900 dark:text-white">
Priya N.
</span>
<span className="block text-[11px] text-slate-500 dark:text-slate-400">
Cargo bike · {kmAway.toFixed(1)} km to go
</span>
</span>
</div>
</div>
<div className="flex items-center gap-3 border-t border-slate-200 bg-slate-50/80 px-3 py-3 sm:px-4 dark:border-slate-800 dark:bg-slate-900/60">
<button
type="button"
aria-pressed={playing}
onClick={togglePlay}
className={`inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-slate-900 px-3 py-2 text-xs font-medium text-white transition-colors hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 ${focusRing}`}
>
{playing ? (
<svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5 fill-current">
<rect x="3.5" y="2.5" width="3" height="11" rx="1" />
<rect x="9.5" y="2.5" width="3" height="11" rx="1" />
</svg>
) : (
<svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5 fill-current">
<path d="M4.5 2.8a1 1 0 0 1 1.52-.85l7.2 4.35a1 1 0 0 1 0 1.71l-7.2 4.35a1 1 0 0 1-1.52-.86z" />
</svg>
)}
{playing ? "Pause" : "Replay route"}
</button>
<label htmlFor="mdt-scrub" className="sr-only">
Courier position along the route
</label>
<input
id="mdt-scrub"
type="range"
min={0}
max={100}
step={1}
value={pct}
aria-valuetext={
delivered ? "Route complete, parcel delivered" : `${pct}% of route, ${minsAway} minutes away`
}
onChange={(e) => {
setPlaying(false);
setProgress(Number(e.target.value) / 100);
}}
className={`h-1.5 w-full flex-1 cursor-pointer appearance-none rounded-full bg-slate-200 accent-indigo-600 dark:bg-slate-700 dark:accent-indigo-400 ${focusRing}`}
/>
<span className="w-10 shrink-0 text-right text-xs font-medium tabular-nums text-slate-500 dark:text-slate-400">
{pct}%
</span>
</div>
</motion.div>
<motion.div
initial={reduce ? false : { opacity: 0, y: 18 }}
whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.55, delay: 0.08, ease: "easeOut" }}
className="flex flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900/50"
>
<div role="status" className="border-b border-slate-200 pb-5 dark:border-slate-800">
{delivered ? (
<p className="text-2xl font-semibold tracking-tight text-emerald-600 dark:text-emerald-400">
Delivered at {arrival}
</p>
) : (
<p className="flex items-baseline gap-1.5">
<span className="text-4xl font-semibold tracking-tight text-slate-900 tabular-nums dark:text-white">
{minsAway}
</span>
<span className="text-sm font-medium text-slate-500 dark:text-slate-400">
min away
</span>
</p>
)}
<p className="mt-1.5 text-sm text-slate-500 dark:text-slate-400">
{delivered
? "Signed for by resident at Unit 14-02."
: `Arrives by ${arrival} · ${kmAway.toFixed(1)} km remaining`}
</p>
<p className="mt-3 text-xs font-medium text-indigo-600 dark:text-indigo-400">
{current.title}
</p>
</div>
<ol className="mt-4 flex-1">
{STOPS.map((s, i) => {
const done = s.t <= progress + 0.001;
const isCurrent = s.id === current.id;
return (
<motion.li
key={s.id}
initial={reduce ? false : { opacity: 0, x: -8 }}
whileInView={reduce ? undefined : { opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-40px" }}
transition={{ duration: 0.35, delay: 0.12 + i * 0.06, ease: "easeOut" }}
className="relative"
>
{i < STOPS.length - 1 && (
<span
aria-hidden="true"
className={`absolute left-[17px] top-7 bottom-0 w-0.5 rounded-full ${
STOPS[i + 1].t <= progress + 0.001
? "bg-indigo-500 dark:bg-indigo-400"
: "bg-slate-200 dark:bg-slate-800"
}`}
/>
)}
<button
type="button"
aria-current={isCurrent ? "step" : undefined}
onClick={() => {
setPlaying(false);
setProgress(s.t);
}}
className={`relative flex w-full items-start gap-3 rounded-lg p-2 text-left transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/60 ${focusRing}`}
>
<span
className={`mt-0.5 grid h-[18px] w-[18px] shrink-0 place-items-center rounded-full border-2 transition-colors ${
done
? "border-indigo-500 bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-400"
: "border-slate-300 bg-white dark:border-slate-700 dark:bg-slate-900"
}`}
>
{done && (
<svg viewBox="0 0 12 12" aria-hidden="true" className="h-2.5 w-2.5">
<path
d="M2.5 6.2 4.8 8.5 9.5 3.5"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="stroke-white dark:stroke-slate-900"
/>
</svg>
)}
</span>
<span className="min-w-0 flex-1">
<span
className={`block text-sm font-medium ${
done ? "text-slate-900 dark:text-white" : "text-slate-400 dark:text-slate-500"
}`}
>
{s.title}
{isCurrent && !delivered && (
<span className="ml-2 inline-flex items-center rounded-full bg-indigo-50 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-700 dark:bg-indigo-950/70 dark:text-indigo-300">
Now
</span>
)}
</span>
<span className="mt-0.5 block text-xs leading-relaxed text-slate-500 dark:text-slate-400">
{s.detail}
</span>
</span>
<span
className={`shrink-0 pt-0.5 text-xs tabular-nums ${
done ? "text-slate-500 dark:text-slate-400" : "text-slate-400 dark:text-slate-600"
}`}
>
{fmtTime(START_MIN + s.t * TRIP_MIN)}
</span>
</button>
</motion.li>
);
})}
</ol>
<div className="mt-5 flex items-center justify-between gap-4 rounded-xl border border-slate-200 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-900/70">
<span className="min-w-0">
<span id="mdt-notify-label" className="block text-sm font-medium text-slate-900 dark:text-white">
Ping me at 5 minutes
</span>
<span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">
Push alert when Priya turns onto your street
</span>
</span>
<button
type="button"
role="switch"
aria-checked={notify}
aria-labelledby="mdt-notify-label"
onClick={() => setNotify((v) => !v)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${focusRing} ${
notify ? "bg-indigo-600 dark:bg-indigo-500" : "bg-slate-300 dark:bg-slate-700"
}`}
>
<span
aria-hidden="true"
className={`inline-block h-[1.125rem] w-[1.125rem] rounded-full bg-white shadow transition-transform ${
notify ? "translate-x-[1.375rem]" : "translate-x-1"
}`}
/>
</button>
</div>
<p className="mt-4 text-xs text-slate-500 dark:text-slate-400">
Delivery PIN{" "}
<span className="font-semibold tabular-nums text-slate-900 dark:text-white">4821</span> ·
Priya will call from the lobby if nobody answers.
</p>
</motion.div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Map Card
Originallocation card with a hand-drawn SVG map

Map Location Picker
Originallocation picker with pin

Map Store Locator
Originalstore locator list plus map

Map Contact
Originalcontact section with map and details

Map Pins List
Originalmap with a list of pinned places

Map Region Select
Originalclickable region selector map

Map Mini
Originalmini map preview widget

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

