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 Progress

#

Pass progress (0 to 1) and a RouteProgress child paints the covered part of the line. RouteMarker pins a marker at "start", "end", "progress", or any fraction.

Progress45%
"use client";

import { useState } from "react";
import {
  Map,
  MapRoute,
  MarkerContent,
  MarkerLabel,
  RouteMarker,
  RouteProgress,
} from "@/components/ui/map";
import { Car } from "lucide-react";
import { Slider } from "@/components/ui/slider";

const route: [number, number][] = [
  [-122.394, 37.7953],
  [-122.3952, 37.7967],
  [-122.397, 37.7986],
  [-122.3975, 37.7992],
  [-122.3976, 37.7993],
  [-122.3981, 37.799],
  [-122.3984, 37.7989],
  [-122.4066, 37.7979],
  [-122.4071, 37.7981],
  [-122.4072, 37.7982],
  [-122.4072, 37.7984],
  [-122.4082, 37.8034],
  [-122.4064, 37.8037],
  [-122.4063, 37.8036],
  [-122.4063, 37.8034],
  [-122.4067, 37.8032],
  [-122.4067, 37.803],
  [-122.4067, 37.8028],
  [-122.4064, 37.8025],
  [-122.4062, 37.802],
  [-122.406, 37.8019],
  [-122.4058, 37.8018],
  [-122.4056, 37.8018],
  [-122.4055, 37.8019],
  [-122.4054, 37.8021],
  [-122.4056, 37.8025],
];

export function RouteProgressExample() {
  const [progress, setProgress] = useState(0.45);

  return (
    <div className="relative h-[420px] w-full">
      <Map center={[-122.4008, 37.7996]} zoom={14.2}>
        <MapRoute
          coordinates={route}
          progress={progress}
          color="#94a3b8"
          width={5}
          opacity={0.8}
          dashArray={[0.5, 1.5]}
        >
          <RouteProgress color="#3b82f6" width={5} opacity={1} />

          <RouteMarker at="start">
            <MarkerContent>
              <div className="border-foreground bg-background size-3.5 rounded-full border-2 shadow-md" />
            </MarkerContent>
          </RouteMarker>

          <RouteMarker at="progress">
            <MarkerContent>
              <div className="ring-background grid size-6 place-items-center rounded-full bg-blue-500 shadow-md ring-2">
                <Car className="size-3 text-white" />
              </div>
              <MarkerLabel
                position="top"
                className="bg-background/90 border-border/50 rounded-md border px-1.5 py-0.5 tabular-nums shadow-sm"
              >
                {Math.round(progress * 100)}%
              </MarkerLabel>
            </MarkerContent>
          </RouteMarker>

          <RouteMarker at="end">
            <MarkerContent>
              <div className="bg-foreground ring-background size-3.5 rounded-full shadow-md ring-2" />
            </MarkerContent>
          </RouteMarker>
        </MapRoute>
      </Map>

      <div className="bg-background/95 border-border/50 absolute bottom-3 left-3 w-56 rounded-lg border p-3 shadow-lg backdrop-blur-md">
        <div className="mb-2 flex items-center justify-between text-xs">
          <span className="font-medium">Progress</span>
          <span className="text-muted-foreground tabular-nums">
            {Math.round(progress * 100)}%
          </span>
        </div>
        <Slider
          value={[progress]}
          onValueChange={([value]) => setProgress(value)}
          min={0}
          max={1}
          step={0.01}
          aria-label="Route progress"
        />
      </div>
    </div>
  );
}

Route Planning

#

Render one MapRoute per option and mark the selected one active, it moves on top and uses the active* styles. Click a line or a row to switch.

"use client";

import { useEffect, useState } from "react";
import { Map, MapMarker, MarkerContent, MapRoute } from "@/components/ui/map";
import { cn } from "@/lib/utils";

const start = { name: "Amsterdam", lng: 4.9041, lat: 52.3676 };
const end = { name: "Rotterdam", lng: 4.4777, lat: 51.9244 };

// One color for every route: the selected one separates itself by weight and
// opacity, not hue. Shared by the lines and the list, so a swatch always
// matches its route.
const routeColor = "#3b82f6";
const inactiveOpacity = 0.35;

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();
  }, []);

  return (
    <div className="relative h-[500px] w-full">
      <Map center={[4.69, 52.14]} zoom={8.5} loading={isLoading}>
        {routes.map((route, index) => (
          <MapRoute
            key={index}
            coordinates={route.coordinates}
            active={index === selectedIndex}
            color={routeColor}
            width={5}
            opacity={inactiveOpacity}
            activeWidth={6}
            activeOpacity={1}
            onClick={() => setSelectedIndex(index)}
          />
        ))}

        <MapMarker longitude={start.lng} latitude={start.lat}>
          <MarkerContent>
            <div className="border-foreground bg-background size-3.5 rounded-full border-2 shadow-md" />
          </MarkerContent>
        </MapMarker>

        <MapMarker longitude={end.lng} latitude={end.lat}>
          <MarkerContent>
            <div className="bg-foreground ring-background size-3.5 rounded-full shadow-md ring-2" />
          </MarkerContent>
        </MapMarker>
      </Map>

      {routes.length > 0 && (
        <div
          role="radiogroup"
          aria-label="Route options"
          className="bg-background/95 border-border/50 absolute top-3 left-3 w-48 space-y-0.5 rounded-lg border p-1 shadow-lg backdrop-blur-md"
        >
          {routes.map((route, index) => {
            const isActive = index === selectedIndex;

            return (
              <button
                key={index}
                type="button"
                role="radio"
                aria-checked={isActive}
                onClick={() => setSelectedIndex(index)}
                className={cn(
                  "flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 transition-colors",
                  isActive ? "bg-muted" : "hover:bg-muted/50",
                )}
              >
                <span
                  className="h-4 w-0.5 shrink-0 rounded-full"
                  style={{
                    backgroundColor: routeColor,
                    opacity: isActive ? 1 : inactiveOpacity,
                  }}
                />
                <span
                  className={cn(
                    "text-sm font-medium tabular-nums",
                    !isActive && "text-muted-foreground",
                  )}
                >
                  {formatDuration(route.duration)}
                </span>
                <span className="text-muted-foreground ml-auto text-xs tabular-nums">
                  {formatDistance(route.distance)}
                </span>
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}