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.
<script setup lang="ts">
import { cn } from "@/lib/utils";
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((d) => ({
id: d.name,
from: [hub.lng, hub.lat] as [number, number],
to: [d.lng, d.lat] as [number, number],
}));
</script>
<template>
<div class="h-[420px] w-full">
<ClientOnly>
<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
class="size-3 rounded-full border-2 border-white bg-blue-500 shadow-md"
/>
<MarkerLabel
position="top"
class="bg-background/80 rounded-sm px-1.5 py-0.5 text-[11px] font-semibold backdrop-blur"
>
{{ hub.name }}
</MarkerLabel>
</MarkerContent>
</MapMarker>
<MapMarker
v-for="dest in destinations"
:key="dest.name"
:longitude="dest.lng"
:latitude="dest.lat"
>
<MarkerContent>
<div
:class="
cn(
'size-2 rounded-full border-2 border-white',
'bg-emerald-500 shadow',
)
"
/>
<MarkerLabel position="top">{{ dest.name }}</MarkerLabel>
</MarkerContent>
</MapMarker>
</Map>
</ClientOnly>
</div>
</template>
Interactive Arcs
Combine hoverPaint with the @hover event 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
<script setup lang="ts">
import type { ExpressionSpecification } from "maplibre-gl";
import type { MapArcDatum, MapArcEvent } 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" };
const modeColorExpression: ExpressionSpecification = [
"match",
["get", "mode"],
"air",
modeColors.air,
"sea",
modeColors.sea,
"#888",
];
const selected = ref<{
lane: Lane;
popupLngLat: { longitude: number; latitude: number };
} | null>(null);
const onHover = (event: MapArcEvent<Lane> | null) => {
selected.value = event
? {
lane: event.arc,
popupLngLat: { longitude: event.longitude, latitude: event.latitude },
}
: null;
};
const endpoints = computed(() => {
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;
});
</script>
<template>
<div class="relative h-[420px] w-full">
<ClientOnly>
<Map :center="[20, 20]" :zoom="0.8">
<MapArc
:data="lanes"
:paint="{ 'line-color': modeColorExpression, 'line-width': 1.5 }"
:hover-paint="{ 'line-width': 3, 'line-opacity': 1 }"
@hover="onHover"
/>
<MapMarker
v-for="point in endpoints"
:key="point.name"
:longitude="point.coords[0]"
:latitude="point.coords[1]"
>
<MarkerContent>
<div class="bg-foreground/80 size-2 rounded-full shadow-sm" />
<MarkerLabel
position="top"
class="text-foreground/80 tracking-tight"
>
{{ point.name }}
</MarkerLabel>
</MarkerContent>
</MapMarker>
<MapPopup
v-if="selected"
:longitude="selected.popupLngLat.longitude"
:latitude="selected.popupLngLat.latitude"
:offset="12"
:close-on-click="false"
class="p-0"
>
<div class="flex items-center gap-2 px-2.5 py-1.5 text-xs">
<span
class="size-1.5 rounded-full"
:style="{
background:
selected.lane.mode === 'air'
? modeColors.air
: modeColors.sea,
}"
/>
<span class="font-medium">
{{ selected.lane.origin }} → {{ selected.lane.destination }}
</span>
<span class="text-muted-foreground border-l pl-2">
{{ selected.lane.volume }}
</span>
</div>
</MapPopup>
</Map>
</ClientOnly>
<div
class="bg-background/80 absolute bottom-3 left-3 z-10 flex items-center gap-3 rounded-full border px-3 py-0.5 text-[11px] shadow-sm backdrop-blur"
>
<div class="flex items-center gap-1.5">
<span
class="size-1.5 rounded-full"
:style="{ background: modeColors.air }"
/>
Air
</div>
<span class="bg-border h-3 w-px" />
<div class="flex items-center gap-1.5">
<span
class="size-1.5 rounded-full"
:style="{ background: modeColors.sea }"
/>
Sea
</div>
</div>
</div>
</template>