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.
<script setup lang="ts">
const route: [number, number][] = [
[-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
];
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 },
];
</script>
<template>
<div class="h-[420px] w-full">
<ClientOnly>
<Map :center="[-73.98, 40.75]" :zoom="11.2">
<MapRoute
:coordinates="route"
color="#3b82f6"
:width="4"
:opacity="0.8"
/>
<MapMarker
v-for="(stop, index) in stops"
:key="stop.name"
:longitude="stop.lng"
:latitude="stop.lat"
>
<MarkerContent>
<div
class="flex size-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>
</ClientOnly>
</div>
</template>
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.
<script setup lang="ts">
import { Clock, Loader2, Route as RouteIcon } from "lucide-vue-next";
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;
distance: number;
}
const routes = ref<RouteData[]>([]);
const selectedIndex = ref(0);
const isLoading = ref(true);
function formatDuration(seconds: number): string {
const mins = Math.round(seconds / 60);
if (mins < 60) return `${mins} min`;
const hours = Math.floor(mins / 60);
return `${hours}h ${mins % 60}m`;
}
function formatDistance(meters: number): string {
if (meters < 1000) return `${Math.round(meters)} m`;
return `${(meters / 1000).toFixed(1)} km`;
}
onMounted(async () => {
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) {
routes.value = data.routes.map(
(r: {
geometry: { coordinates: [number, number][] };
duration: number;
distance: number;
}) => ({
coordinates: r.geometry.coordinates,
duration: r.duration,
distance: r.distance,
}),
);
}
} catch (err) {
console.error("Failed to fetch routes:", err);
} finally {
isLoading.value = false;
}
});
// Re-order so the selected route renders on top.
const sortedRoutes = computed(() =>
routes.value
.map((route, index) => ({ route, index }))
.sort((a, b) => {
if (a.index === selectedIndex.value) return 1;
if (b.index === selectedIndex.value) return -1;
return 0;
}),
);
</script>
<template>
<div class="relative h-[500px] w-full">
<ClientOnly>
<Map :center="[4.69, 52.14]" :zoom="8.5">
<MapRoute
v-for="{ route, index } in sortedRoutes"
:key="index"
:coordinates="route.coordinates"
:color="index === selectedIndex ? '#6366f1' : '#94a3b8'"
:width="index === selectedIndex ? 6 : 5"
:opacity="index === selectedIndex ? 1 : 0.6"
@click="selectedIndex = index"
/>
<MapMarker :longitude="start.lng" :latitude="start.lat">
<MarkerContent>
<div
class="size-5 rounded-full border-2 border-white bg-green-500 shadow-lg"
/>
<MarkerLabel position="top">{{ start.name }}</MarkerLabel>
</MarkerContent>
</MapMarker>
<MapMarker :longitude="end.lng" :latitude="end.lat">
<MarkerContent>
<div
class="size-5 rounded-full border-2 border-white bg-red-500 shadow-lg"
/>
<MarkerLabel position="bottom">{{ end.name }}</MarkerLabel>
</MarkerContent>
</MapMarker>
</Map>
</ClientOnly>
<div
v-if="routes.length > 0"
class="absolute top-3 left-3 z-10 flex flex-col gap-2"
>
<button
v-for="(route, index) in routes"
:key="index"
type="button"
:class="[
'inline-flex h-9 items-center justify-start gap-3 rounded-md px-3 text-xs font-medium shadow-sm transition-colors',
index === selectedIndex
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
]"
@click="selectedIndex = index"
>
<span class="flex items-center gap-1.5">
<Clock class="size-3.5" />
<span>{{ formatDuration(route.duration) }}</span>
</span>
<span class="flex items-center gap-1.5 text-xs opacity-80">
<RouteIcon class="size-3" />
{{ formatDistance(route.distance) }}
</span>
<span
v-if="index === 0"
class="rounded bg-green-100 px-1.5 py-0.5 text-[10px] font-medium text-green-700 dark:bg-green-900 dark:text-green-300"
>
Fastest
</span>
</button>
</div>
<div
v-if="isLoading"
class="bg-background/50 absolute inset-0 z-20 flex items-center justify-center"
>
<Loader2 class="text-muted-foreground size-6 animate-spin" />
</div>
</div>
</template>