Data Storytelling

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.

How to install Pro components →See the plans →

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>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

0.45
2.4
0.6
7
Active edited usageDefault props
<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

PropTypeDefaultDescription
pointsRouteMapPoint[]—Route vertices, connected in order, at least two; coordinates are 0–1 relative values
roadsRouteMapRoad[]—Extra decorative roads, drawn above the deterministic streets and below the route
seednumber7Random seed for basemap streets, parks and water; the same seed always produces the same map
progressnumber—Controlled progress (0–1); sets the marker position and travelled segment directly, and disables autoplay
defaultProgressnumber0Starting 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
playingboolean—Play/pause when trigger="manual"; resuming continues from the current position, and replays from the start once finished
durationnumber2.4Route draw duration (seconds), also written to --wb-rmc-duration
travelFactornumber3Travel duration multiplier: travel time = duration × travelFactor, linear
glownumber0.6Strength of the glow under the route (0–1)
totalMinutesnumber18Estimated minutes for the whole trip; minutes left = round(totalMinutes × (1 − progress))
totalKmnumber—Total distance in km; when provided the ETA readout also shows the remaining distance
smoothbooleanfalseWhen true, vertices are joined with a Catmull-Rom smooth curve; otherwise a rounded polyline
cornerRadiusnumber12Corner 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
aspectstring"4/3"Map aspect ratio, e.g. "16/9" or "1"
routeColorstring—Route colour; falls back to --wb-rmc-route (default #3b82f6)
routeNamestring"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
headerReactNode—Content at the top of the card, e.g. courier info
classNamestring—Passed through to the outermost container

RouteMapPoint

FieldTypeDefaultDescription
xnumber—Horizontal position (0 = left edge, 1 = right edge)
ynumber—Vertical position (0 = top edge, 1 = bottom edge)
labelstring—Street or place name; fades in when the marker arrives, start and end are always shown

RouteMapRoad

FieldTypeDefaultDescription
dstring—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
widthnumber10Stroke 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; smooth switches 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 CSS offset-path and a scaled viewBox
  • Drawing: the route uses pathLength="1" with a CSS transition on stroke-dashoffset; the duration reads --wb-rmc-duration and the curve reads --wb-rmc-ease (ease-in-out by default). Beneath it sits a thicker, Gaussian-blurred copy of the route whose opacity equals glow
  • Travelled segment: a top copy of the same path uses stroke-dasharray to reveal only the first progress of 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: seed drives a tiny mulberry32 PRNG that generates 10–14 streets, two parks and one body of water, with no Math.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 an aria-label generated from props, e.g. "Delivery route, from Old Zhang Noodles to You, arriving in 12 minutes"
  • The ETA area carries sr-only text in an aria-live="polite" region that only changes when a whole minute ticks down, so screen readers are not interrupted every frame; the rolling digits themselves are aria-hidden
  • The basemap, route and street labels are decorative (aria-hidden); the full information is already in the aria-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