Route Map Card
A navigation-style route card — a grid basemap with deterministic streets, a route drawn segment by segment, a marker travelling along it, street labels fading in on arrival and a live ETA readout.
This is a WebberUI Pro component
The install command below needs a licence to run; without one it returns 401.
Delivery tracking, ride hailing, shipment progress — any "it is on its way" screen fits in one card. Route vertices are 0–1 relative coordinates, so no real map data or tiles are needed: the basemap is a two-level CSS grid plus deterministic streets, parks and water generated from seed. The route is drawn first, then the marker travels along it, street names fade in as it passes, and a glass readout in the corner rolls the remaining minutes and distance digit by digit. Progress can autoplay inside the component or be fully controlled through progress, so you can wire in your own live location data.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add "https://webberui.com/r/route-map-card.en.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<RouteMapCard/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add "https://webberui.com/r/route-map-card.en.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/route-map-card.
Usage
import {
RouteMapCard,
type RouteMapPoint,
} from "@/components/ui/route-map-card";
const route: RouteMapPoint[] = [
{ x: 0.14, y: 0.8, label: "Old Zhang Noodles" },
{ x: 0.16, y: 0.46, label: "Minsheng E. Rd" },
{ x: 0.46, y: 0.46, label: "Fuxing N. Rd" },
{ x: 0.5, y: 0.22, label: "Nanjing E. Rd" },
{ x: 0.78, y: 0.24, label: "Alley corner" },
{ x: 0.84, y: 0.56, label: "You" },
];
// Autoplay: once scrolled into view, draw the route, then travel it over duration × travelFactor
<RouteMapCard points={route} totalMinutes={12} totalKm={3.6} routeName="Delivery route" />;
// Controlled: feed in your own live progress (0–1)
<RouteMapCard points={route} progress={0.42} totalMinutes={12} />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
points | RouteMapPoint[] | — | Route vertices, connected in order, at least two; coordinates are 0–1 relative values |
roads | RouteMapRoad[] | — | Extra decorative roads, drawn above the deterministic streets and below the route |
seed | number | 7 | Random seed for basemap streets, parks and water; the same seed always produces the same map |
progress | number | — | Controlled progress (0–1); sets the marker position and travelled segment directly, and disables autoplay |
defaultProgress | number | 0 | Starting progress in uncontrolled mode |
onProgressChange | (progress: number) => void | — | Reports progress on every frame during uncontrolled autoplay |
onArrive | () => void | — | Called when uncontrolled autoplay reaches the destination |
trigger | "mount" | "inView" | "manual" | "inView" | When uncontrolled playback starts: on mount, on scroll into view (about 30% visible) or manually |
playing | boolean | — | Play/pause when trigger="manual"; resuming continues from the current position, and replays from the start once finished |
duration | number | 2.4 | Route draw duration (seconds), also written to --wb-rmc-duration |
travelFactor | number | 3 | Travel duration multiplier: travel time = duration × travelFactor, linear |
glow | number | 0.6 | Strength of the glow under the route (0–1) |
totalMinutes | number | 18 | Estimated minutes for the whole trip; minutes left = round(totalMinutes × (1 − progress)) |
totalKm | number | — | Total distance in km; when provided the ETA readout also shows the remaining distance |
smooth | boolean | false | When true, vertices are joined with a Catmull-Rom smooth curve; otherwise a rounded polyline |
cornerRadius | number | 12 | Corner radius of each turn in the rounded polyline (viewBox units, roughly px) |
markerShape | "dot" | "arrow" | "arrow" | Marker shape; the arrow points in the direction of travel |
aspect | string | "4/3" | Map aspect ratio, e.g. "16/9" or "1" |
routeColor | string | — | Route colour; falls back to --wb-rmc-route (default #3b82f6) |
routeName | string | "Route" | Route name that opens the screen reader description |
etaCorner | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "bottom-right" | ETA readout position, so it stays clear of your route |
header | ReactNode | — | Content at the top of the card, e.g. courier info |
className | string | — | Passed through to the outermost container |
RouteMapPoint
| Field | Type | Default | Description |
|---|---|---|---|
x | number | — | Horizontal position (0 = left edge, 1 = right edge) |
y | number | — | Vertical position (0 = top edge, 1 = bottom edge) |
label | string | — | Street or place name; fades in when the marker arrives, start and end are always shown |
RouteMapRoad
| Field | Type | Default | Description |
|---|---|---|---|
d | string | — | Raw SVG path data in viewBox coordinates (width fixed at 400, height = 400 ÷ aspect); use either this or points |
points | [number, number][] | — | Polyline vertices as 0–1 relative coordinates |
width | number | 10 | Stroke width (viewBox units) |
The RouteMapCardProps, RouteMapPoint, RouteMapRoad, RouteMapTrigger, RouteMapMarkerShape and RouteMapEtaCorner types are exported as well.
How it works
- Coordinate space: the SVG viewBox is 400 wide with a height derived from
aspect, and the map container uses the same aspect ratio, so 0–1 coordinates, the SVG and the HTML labels line up exactly - Path: by default a rounded polyline, with each corner rounded by a quadratic Bézier whose radius never exceeds half of either adjacent segment;
smoothswitches to a Catmull-Rom curve (converted to cubic Béziers) through every vertex - Marker placement: the component samples the path into a polyline with cumulative arc length in JS and interpolates position and heading from progress — the same result as
getPointAtLength, but pure computation, correct on the first SSR render, and free of the mismatch between CSSoffset-pathand a scaled viewBox - Drawing: the route uses
pathLength="1"with a CSS transition onstroke-dashoffset; the duration reads--wb-rmc-durationand the curve reads--wb-rmc-ease(ease-in-out by default). Beneath it sits a thicker, Gaussian-blurred copy of the route whose opacity equalsglow - Travelled segment: a top copy of the same path uses
stroke-dasharrayto reveal only the firstprogressof the route, and the corner dots it passes fill in - Street labels: each vertex's cumulative length fraction decides when it is reached; on arrival the label fades in over 200ms while rising 4px
- Deterministic basemap:
seeddrives a tiny mulberry32 PRNG that generates 10–14 streets, two parks and one body of water, with noMath.random, so server and browser output match - ETA readout: the minutes left roll digit by digit (one 0–9 column per digit); the remaining distance is
totalKm × (1 − progress)to one decimal place; on arrival it reads "Arrived"
Accessibility
- The map SVG is
role="img"with anaria-labelgenerated from props, e.g. "Delivery route, from Old Zhang Noodles to You, arriving in 12 minutes" - The ETA area carries
sr-onlytext in anaria-live="polite"region that only changes when a whole minute ticks down, so screen readers are not interrupted every frame; the rolling digits themselves arearia-hidden - The basemap, route and street labels are decorative (
aria-hidden); the full information is already in thearia-label - When the user has reduce motion turned on, the route appears fully drawn with no draw animation, the marker sits directly at
progress(at the destination when uncontrolled), the pulsing ring stops, all street labels show at once and the digits no longer roll
Sankey Flow
Sankey diagram: automatic column layout, link widths proportional to value, animated gradient flows, and hover that traces a full path while dimming the rest — self-contained layout, no d3
Duration Compare Bars
A duration comparison React component: two to five durations grow on one shared scale at one shared speed — say 30 minutes versus six weeks. Units convert automatically, readouts count up with the bar width, a ratio badge pops in when they finish, and a broken axis keeps the shortest bar visible when the gap is extreme.