Routes
Draw lines and paths connecting coordinates on the map.
Use MapRoute to draw lines connecting a series of coordinates. Perfect for showing directions, trails, or any path between points.
Basic Route
Draw a route with numbered stop markers along the path.
import {
Map,
MapMarker,
MarkerContent,
MarkerTooltip,
MapRoute,
} from "@/components/ui/map";
const route = [
[-74.006, 40.7128], // NYC City Hall
[-73.9857, 40.7484], // Empire State Building
[-73.9772, 40.7527], // Grand Central
[-73.9654, 40.7829], // Central Park
] as [number, number][];
const stops = [
{ name: "City Hall", lng: -74.006, lat: 40.7128 },
{ name: "Empire State Building", lng: -73.9857, lat: 40.7484 },
{ name: "Grand Central Terminal", lng: -73.9772, lat: 40.7527 },
{ name: "Central Park", lng: -73.9654, lat: 40.7829 },
];
export function RouteExample() {
return (
<div className="h-[420px] w-full">
<Map center={[-73.98, 40.75]} zoom={11.2}>
<MapRoute coordinates={route} color="#3b82f6" width={4} opacity={0.8} />
{stops.map((stop, index) => (
<MapMarker key={stop.name} longitude={stop.lng} latitude={stop.lat}>
<MarkerContent>
<div className="flex size-4.5 items-center justify-center rounded-full border-2 border-white bg-blue-500 text-xs font-semibold text-white shadow-lg">
{index + 1}
</div>
</MarkerContent>
<MarkerTooltip>{stop.name}</MarkerTooltip>
</MapMarker>
))}
</Map>
</div>
);
}
Route Planning
Display multiple route options and let users select between them. This example fetches real driving directions from the OSRM API. Click on a route or use the buttons to switch.
"use client";
import { useEffect, useState } from "react";
import {
Map,
MapMarker,
MarkerContent,
MapRoute,
MarkerLabel,
} from "@/components/ui/map";
import { Loader2, Clock, Route } from "lucide-react";
import { Button } from "@/components/ui/button";
const start = { name: "Amsterdam", lng: 4.9041, lat: 52.3676 };
const end = { name: "Rotterdam", lng: 4.4777, lat: 51.9244 };
interface RouteData {
coordinates: [number, number][];
duration: number; // seconds
distance: number; // meters
}
function formatDuration(seconds: number): string {
const mins = Math.round(seconds / 60);
if (mins < 60) return `${mins} min`;
const hours = Math.floor(mins / 60);
const remainingMins = mins % 60;
return `${hours}h ${remainingMins}m`;
}
function formatDistance(meters: number): string {
if (meters < 1000) return `${Math.round(meters)} m`;
return `${(meters / 1000).toFixed(1)} km`;
}
export function OsrmRouteExample() {
const [routes, setRoutes] = useState<RouteData[]>([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function fetchRoutes() {
try {
const response = await fetch(
`https://router.project-osrm.org/route/v1/driving/${start.lng},${start.lat};${end.lng},${end.lat}?overview=full&geometries=geojson&alternatives=true`
);
const data = await response.json();
if (data.routes?.length > 0) {
const routeData: RouteData[] = data.routes.map(
(route: {
geometry: { coordinates: [number, number][] };
duration: number;
distance: number;
}) => ({
coordinates: route.geometry.coordinates,
duration: route.duration,
distance: route.distance,
})
);
setRoutes(routeData);
}
} catch (error) {
console.error("Failed to fetch routes:", error);
} finally {
setIsLoading(false);
}
}
fetchRoutes();
}, []);
// Sort routes: non-selected first, selected last (renders on top)
const sortedRoutes = routes
.map((route, index) => ({ route, index }))
.sort((a, b) => {
if (a.index === selectedIndex) return 1;
if (b.index === selectedIndex) return -1;
return 0;
});
return (
<div className="h-[500px] w-full relative">
<Map center={[4.69, 52.14]} zoom={8.5}>
{sortedRoutes.map(({ route, index }) => {
const isSelected = index === selectedIndex;
return (
<MapRoute
key={index}
coordinates={route.coordinates}
color={isSelected ? "#6366f1" : "#94a3b8"}
width={isSelected ? 6 : 5}
opacity={isSelected ? 1 : 0.6}
onClick={() => setSelectedIndex(index)}
/>
);
})}
<MapMarker longitude={start.lng} latitude={start.lat}>
<MarkerContent>
<div className="size-5 rounded-full bg-green-500 border-2 border-white shadow-lg" />
<MarkerLabel position="top">{start.name}</MarkerLabel>
</MarkerContent>
</MapMarker>
<MapMarker longitude={end.lng} latitude={end.lat}>
<MarkerContent>
<div className="size-5 rounded-full bg-red-500 border-2 border-white shadow-lg" />
<MarkerLabel position="bottom">{end.name}</MarkerLabel>
</MarkerContent>
</MapMarker>
</Map>
{routes.length > 0 && (
<div className="absolute top-3 left-3 flex flex-col gap-2">
{routes.map((route, index) => {
const isActive = index === selectedIndex;
const isFastest = index === 0;
return (
<Button
key={index}
variant={isActive ? "default" : "secondary"}
size="sm"
onClick={() => setSelectedIndex(index)}
className="justify-start gap-3"
>
<div className="flex items-center gap-1.5">
<Clock className="size-3.5" />
<span className="font-medium">
{formatDuration(route.duration)}
</span>
</div>
<div className="flex items-center gap-1.5 text-xs opacity-80">
<Route className="size-3" />
{formatDistance(route.distance)}
</div>
{isFastest && (
<span className="text-[10px] px-1.5 py-0.5 rounded font-medium bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300">
Fastest
</span>
)}
</Button>
);
})}
</div>
)}
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-background/50">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
)}
</div>
);
}