Arcs
Draw curved connections between two coordinates with hover and click support.
Use MapArc to draw curved lines between coordinate pairs. Arcs are great for showing flight paths, shipping lanes, or any origin–destination connection where a straight line would feel flat.
Basic Arc
Pass an array of arcs to the data prop. Each arc needs a unique id and from / to coordinates as [longitude, latitude] tuples.
import { cn } from "@/lib/utils";
import {
Map,
MapArc,
MapMarker,
MarkerContent,
MarkerLabel,
} from "@/components/ui/map";
const hub = { name: "London", lng: -0.1276, lat: 51.5074 };
const destinations = [
{ name: "New York", lng: -74.006, lat: 40.7128 },
{ name: "São Paulo", lng: -46.6333, lat: -23.5505 },
{ name: "Cape Town", lng: 18.4241, lat: -33.9249 },
{ name: "Dubai", lng: 55.2708, lat: 25.2048 },
{ name: "Mumbai", lng: 72.8777, lat: 19.076 },
{ name: "Singapore", lng: 103.8198, lat: 1.3521 },
{ name: "Tokyo", lng: 139.6917, lat: 35.6895 },
{ name: "Sydney", lng: 151.2093, lat: -33.8688 },
];
const arcs = destinations.map((dest) => ({
id: dest.name,
from: [hub.lng, hub.lat] as [number, number],
to: [dest.lng, dest.lat] as [number, number],
}));
export function ArcExample() {
return (
<div className="h-[420px] w-full">
<Map center={[hub.lng, hub.lat]} zoom={1} projection={{ type: "globe" }}>
<MapArc
data={arcs}
paint={{
"line-color": "#3b82f6",
"line-dasharray": [2, 2],
}}
interactive={false}
/>
<MapMarker longitude={hub.lng} latitude={hub.lat}>
<MarkerContent>
<div className="size-3 rounded-full border-2 border-white bg-blue-500 shadow-md" />
<MarkerLabel
position="top"
className="bg-background/80 rounded-sm px-1.5 py-0.5 text-[11px] font-semibold backdrop-blur"
>
{hub.name}
</MarkerLabel>
</MarkerContent>
</MapMarker>
{destinations.map((dest) => (
<MapMarker key={dest.name} longitude={dest.lng} latitude={dest.lat}>
<MarkerContent>
<div
className={cn(
"size-2 rounded-full border-2 border-white",
"bg-emerald-500 shadow",
)}
/>
<MarkerLabel position="top">{dest.name}</MarkerLabel>
</MarkerContent>
</MapMarker>
))}
</Map>
</div>
);
}
Interactive Arcs
Combine hoverPaint with onHover to highlight an arc and surface details in a MapPopup. Use a match expression on line-color to style arcs by category. Here, air and sea lanes are styled differently.
Air
Sea
"use client";
import { useMemo, useState } from "react";
import type { ExpressionSpecification } from "maplibre-gl";
import {
Map,
MapArc,
MapMarker,
MapPopup,
MarkerContent,
MarkerLabel,
type MapArcDatum,
} from "@/components/ui/map";
interface Lane extends MapArcDatum {
origin: string;
destination: string;
volume: string;
mode: "air" | "sea";
}
const lanes: Lane[] = [
{
id: "shg-lax",
origin: "Shanghai",
destination: "Los Angeles",
from: [121.4737, 31.2304],
to: [-118.2437, 34.0522],
volume: "24.8k TEU",
mode: "sea",
},
{
id: "sin-rtm",
origin: "Singapore",
destination: "Rotterdam",
from: [103.8198, 1.3521],
to: [4.4777, 51.9244],
volume: "9.4k TEU",
mode: "sea",
},
{
id: "san-cpt",
origin: "Santos",
destination: "Cape Town",
from: [-46.3322, -23.9608],
to: [18.4241, -33.9249],
volume: "3.2k TEU",
mode: "sea",
},
{
id: "syd-nrt",
origin: "Sydney",
destination: "Tokyo",
from: [151.2093, -33.8688],
to: [139.6917, 35.6895],
volume: "640 tons",
mode: "air",
},
{
id: "dxb-jfk",
origin: "Dubai",
destination: "New York",
from: [55.2708, 25.2048],
to: [-74.006, 40.7128],
volume: "980 tons",
mode: "air",
},
{
id: "dxb-bom",
origin: "Dubai",
destination: "Mumbai",
from: [55.2708, 25.2048],
to: [72.8777, 19.076],
volume: "1.2k tons",
mode: "sea",
},
];
const modeColors = {
air: "#a78bfa",
sea: "#34d399",
};
interface SelectedLane {
lane: Lane;
popupLngLat: { longitude: number; latitude: number };
}
const modeColorExpression: ExpressionSpecification = [
"match",
["get", "mode"],
"air",
modeColors.air,
"sea",
modeColors.sea,
"#888",
];
export function InteractiveArcExample() {
const [selected, setSelected] = useState<SelectedLane | null>(null);
const endpoints = useMemo(() => {
const points: { name: string; coords: [number, number] }[] = [];
const seen = new Set<string>();
for (const lane of lanes) {
if (!seen.has(lane.origin)) {
seen.add(lane.origin);
points.push({ name: lane.origin, coords: lane.from });
}
if (!seen.has(lane.destination)) {
seen.add(lane.destination);
points.push({ name: lane.destination, coords: lane.to });
}
}
return points;
}, []);
return (
<div className="relative h-[420px] w-full">
<Map center={[20, 20]} zoom={0.8}>
<MapArc<Lane>
data={lanes}
paint={{
"line-color": modeColorExpression,
"line-width": 1.5,
}}
hoverPaint={{
"line-width": 3,
"line-opacity": 1,
}}
onHover={(event) =>
setSelected(
event
? {
lane: event.arc,
popupLngLat: {
longitude: event.longitude,
latitude: event.latitude,
},
}
: null,
)
}
/>
{endpoints.map((point) => (
<MapMarker
key={point.name}
longitude={point.coords[0]}
latitude={point.coords[1]}
>
<MarkerContent>
<div className="bg-foreground/80 size-2 rounded-full shadow-sm" />
<MarkerLabel
position="top"
className="text-foreground/80 tracking-tight"
>
{point.name}
</MarkerLabel>
</MarkerContent>
</MapMarker>
))}
{selected && (
<MapPopup
longitude={selected.popupLngLat.longitude}
latitude={selected.popupLngLat.latitude}
offset={12}
closeOnClick={false}
className="p-0"
>
<div className="flex items-center gap-2 px-2.5 py-1.5 text-xs">
<span
className="size-1.5 rounded-full"
style={{
background:
selected.lane.mode === "air"
? modeColors.air
: modeColors.sea,
}}
/>
<span className="font-medium">
{selected.lane.origin} → {selected.lane.destination}
</span>
<span className="text-muted-foreground border-l pl-2">
{selected.lane.volume}
</span>
</div>
</MapPopup>
)}
</Map>
<div className="bg-background/80 absolute bottom-3 left-3 flex items-center gap-3 rounded-full border px-3 py-0.5 text-[11px] shadow-sm backdrop-blur">
<div className="flex items-center gap-1.5">
<span
className="size-1.5 rounded-full"
style={{ background: modeColors.air }}
/>
Air
</div>
<span className="bg-border h-3 w-px" />
<div className="flex items-center gap-1.5">
<span
className="size-1.5 rounded-full"
style={{ background: modeColors.sea }}
/>
Sea
</div>
</div>
</div>
);
}