Map
Interactive map component with markers, popups, tooltips, routes, and clustering built on MapLibre GL
Installation
Dependencies
Examples
3d
"use client";
import { useEffect, useState } from "react";
import { Map, useMap } from "@/components/vritti/map";
import { RotateCcw, Mountain } from "lucide-react";
function MapController() {
const { map, isLoaded } = useMap();
const [pitch, setPitch] = useState(0);
const [bearing, setBearing] = useState(0);
useEffect(() => {
if (!map || !isLoaded) return;
const handleMove = () => {
setPitch(Math.round(map.getPitch()));
setBearing(Math.round(map.getBearing()));
};
map.on("move", handleMove);
return () => {
map.off("move", handleMove);
};
}, [map, isLoaded]);
const handle3DView = () => {
map?.easeTo({
pitch: 60,
bearing: -20,
duration: 1000,
});
};
const handleReset = () => {
map?.easeTo({
pitch: 0,
bearing: 0,
duration: 1000,
});
};
if (!isLoaded) return null;
return (
<div className="absolute top-3 left-3 z-10 flex flex-col gap-2">
<div className="flex gap-2">
<button
onClick={handle3DView}
className="inline-flex items-center rounded-md bg-secondary text-secondary-foreground px-3 py-1.5 text-sm font-medium shadow-sm hover:bg-secondary/80"
>
<Mountain className="size-4 mr-1.5" />
3D View
</button>
<button
onClick={handleReset}
className="inline-flex items-center rounded-md bg-secondary text-secondary-foreground px-3 py-1.5 text-sm font-medium shadow-sm hover:bg-secondary/80"
>
<RotateCcw className="size-4 mr-1.5" />
Reset
</button>
</div>
<div className="rounded-md bg-background/90 backdrop-blur px-3 py-2 text-xs font-mono border">
<div>Pitch: {pitch}°</div>
<div>Bearing: {bearing}°</div>
</div>
</div>
);
}
export function Map3DExample() {
return (
<div className="h-[400px] w-full overflow-hidden rounded-lg border">
<Map center={[-73.9857, 40.7484]} zoom={15}>
<MapController />
</Map>
</div>
);
}
Cluster
"use client";
import { useState } from "react";
import { Map, MapClusterLayer, MapPopup, MapControls } from "@/components/vritti/map";
interface EarthquakeProperties {
mag: number;
place: string;
tsunami: number;
}
export function MapClusterExample() {
const [selectedPoint, setSelectedPoint] = useState<{
coordinates: [number, number];
properties: EarthquakeProperties;
} | null>(null);
return (
<div className="h-[400px] w-full overflow-hidden rounded-lg border">
<Map center={[-103.59, 40.66]} zoom={3.4} fadeDuration={0}>
<MapClusterLayer<EarthquakeProperties>
data="https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson"
clusterRadius={50}
clusterMaxZoom={14}
clusterColors={["#1d8cf8", "#6d5dfc", "#e23670"]}
pointColor="#1d8cf8"
onPointClick={(feature, coordinates) => {
setSelectedPoint({
coordinates,
properties: feature.properties,
});
}}
/>
{selectedPoint && (
<MapPopup
key={`${selectedPoint.coordinates[0]}-${selectedPoint.coordinates[1]}`}
longitude={selectedPoint.coordinates[0]}
latitude={selectedPoint.coordinates[1]}
onClose={() => setSelectedPoint(null)}
closeOnClick={false}
focusAfterOpen={false}
closeButton
>
<div className="space-y-1 p-1">
<p className="text-sm">
Magnitude: {selectedPoint.properties.mag}
</p>
<p className="text-sm">
Tsunami:{" "}
{selectedPoint.properties?.tsunami === 1 ? "Yes" : "No"}
</p>
</div>
</MapPopup>
)}
<MapControls />
</Map>
</div>
);
}
Controlled
lng: -74.006lat: 40.713zoom: 8.0bearing: 0.0°pitch: 0.0°
"use client";
import { useState } from "react";
import { Map, type MapViewport } from "@/components/vritti/map";
export function MapControlledExample() {
const [viewport, setViewport] = useState<MapViewport>({
center: [-74.006, 40.7128],
zoom: 8,
bearing: 0,
pitch: 0,
});
return (
<div className="h-[400px] w-full relative overflow-hidden rounded-lg border">
<Map viewport={viewport} onViewportChange={setViewport} />
<div className="absolute top-2 left-2 z-10 flex flex-wrap gap-x-3 gap-y-1 text-xs font-mono bg-background/80 backdrop-blur px-2 py-1.5 rounded border">
<span>
<span className="text-muted-foreground">lng:</span>{" "}
{viewport.center[0].toFixed(3)}
</span>
<span>
<span className="text-muted-foreground">lat:</span>{" "}
{viewport.center[1].toFixed(3)}
</span>
<span>
<span className="text-muted-foreground">zoom:</span>{" "}
{viewport.zoom.toFixed(1)}
</span>
<span>
<span className="text-muted-foreground">bearing:</span>{" "}
{viewport.bearing.toFixed(1)}°
</span>
<span>
<span className="text-muted-foreground">pitch:</span>{" "}
{viewport.pitch.toFixed(1)}°
</span>
</div>
</div>
);
}
Controls
import { Map, MapControls } from "@/components/vritti/map";
export function MapControlsExample() {
return (
<div className="h-[400px] w-full overflow-hidden rounded-lg border">
<Map center={[2.3522, 48.8566]} zoom={11}>
<MapControls
position="bottom-right"
showZoom
showCompass
showLocate
showFullscreen
/>
</Map>
</div>
);
}
Delivery
"use client";
import { useEffect, useState } from "react";
import {
Map,
MapMarker,
MarkerContent,
MarkerLabel,
MarkerTooltip,
MapRoute,
MapControls,
} from "@/components/vritti/map";
import { Truck } from "lucide-react";
const store = { lng: -0.14, lat: 51.5154 };
const home = { lng: -0.07, lat: 51.51 };
export function MapDeliveryExample() {
const [route, setRoute] = useState<[number, number][]>([]);
const [truckPosition, setTruckPosition] = useState<[number, number] | null>(
null
);
useEffect(() => {
async function fetchRoute() {
try {
const response = await fetch(
`https://router.project-osrm.org/route/v1/driving/${store.lng},${store.lat};${home.lng},${home.lat}?overview=full&geometries=geojson`
);
const data = await response.json();
if (data.routes?.[0]?.geometry?.coordinates) {
const coords: [number, number][] =
data.routes[0].geometry.coordinates;
setRoute(coords);
const truckIdx = Math.floor(coords.length * 0.6);
setTruckPosition(coords[truckIdx]);
}
} catch (error) {
console.error("Failed to fetch route:", error);
}
}
fetchRoute();
}, []);
return (
<div className="h-[400px] w-full overflow-hidden rounded-lg border">
<Map center={[-0.105, 51.511]} zoom={12.4}>
{route.length > 0 && (
<MapRoute coordinates={route} width={4} color="#4285F4" />
)}
<MapMarker longitude={store.lng} latitude={store.lat}>
<MarkerContent>
<div className="size-3.5 rounded-full bg-emerald-500 border-2 border-white shadow-lg" />
<MarkerLabel>Store</MarkerLabel>
</MarkerContent>
</MapMarker>
{truckPosition && (
<MapMarker longitude={truckPosition[0]} latitude={truckPosition[1]}>
<MarkerContent>
<div className="bg-blue-500 rounded-full p-1.5 shadow-lg">
<Truck className="size-3 text-white" />
</div>
</MarkerContent>
<MarkerTooltip>On the way</MarkerTooltip>
</MapMarker>
)}
<MapMarker longitude={home.lng} latitude={home.lat}>
<MarkerContent>
<div className="size-3.5 rounded-full bg-blue-500 border-2 border-white shadow-lg" />
<MarkerLabel>Home</MarkerLabel>
</MarkerContent>
</MapMarker>
<MapControls showZoom />
</Map>
</div>
);
}
Draggable
"use client";
import { useState } from "react";
import { Map, MapMarker, MarkerContent, MarkerPopup } from "@/components/vritti/map";
import { MapPin } from "lucide-react";
export function MapDraggableExample() {
const [draggableMarker, setDraggableMarker] = useState({
lng: -73.98,
lat: 40.75,
});
return (
<div className="h-[400px] w-full overflow-hidden rounded-lg border">
<Map center={[-73.98, 40.75]} zoom={12}>
<MapMarker
draggable
longitude={draggableMarker.lng}
latitude={draggableMarker.lat}
onDragEnd={(lngLat) => {
setDraggableMarker({ lng: lngLat.lng, lat: lngLat.lat });
}}
>
<MarkerContent>
<div className="cursor-move">
<MapPin
className="fill-black stroke-white dark:fill-white"
size={28}
/>
</div>
</MarkerContent>
<MarkerPopup>
<div className="space-y-1">
<p className="font-medium text-foreground">Coordinates</p>
<p className="text-xs text-muted-foreground">
{draggableMarker.lat.toFixed(4)},{" "}
{draggableMarker.lng.toFixed(4)}
</p>
</div>
</MarkerPopup>
</MapMarker>
</Map>
</div>
);
}
Globe
Active Users
2,847
+12.5%vs last hour
High
Medium
Low
"use client";
import {
Map,
MapMarker,
MarkerContent,
MarkerTooltip,
MapControls,
} from "@/components/vritti/map";
import { TrendingUp } from "lucide-react";
const analyticsData = [
{ lng: -74.006, lat: 40.7128, city: "New York", users: 847, size: 14 },
{ lng: -0.1276, lat: 51.5074, city: "London", users: 623, size: 12 },
{ lng: 139.6917, lat: 35.6895, city: "Tokyo", users: 412, size: 10 },
{ lng: -122.4194, lat: 37.7749, city: "San Francisco", users: 298, size: 9 },
{ lng: 2.3522, lat: 48.8566, city: "Paris", users: 187, size: 8 },
{ lng: 77.209, lat: 28.6139, city: "Delhi", users: 156, size: 7 },
{ lng: 151.2093, lat: -33.8688, city: "Sydney", users: 134, size: 7 },
{ lng: -43.1729, lat: -22.9068, city: "Rio", users: 89, size: 6 },
{ lng: 4.9041, lat: 52.3676, city: "Amsterdam", users: 76, size: 5 },
{ lng: 126.978, lat: 37.5665, city: "Seoul", users: 45, size: 5 },
];
export function MapGlobeExample() {
return (
<div className="h-[450px] w-full relative overflow-hidden rounded-lg border">
<div className="absolute top-3 left-3 z-10 bg-background/95 backdrop-blur-md rounded-lg p-3 border border-border/50 shadow-lg">
<div className="tracking-wider text-[10px] text-muted-foreground uppercase mb-1">
Active Users
</div>
<div className="text-2xl font-semibold leading-tight">2,847</div>
<div className="flex items-center gap-1 mt-1">
<TrendingUp className="size-3 text-emerald-500" />
<span className="text-xs text-emerald-500">+12.5%</span>
<span className="text-xs text-muted-foreground">vs last hour</span>
</div>
</div>
<div className="absolute bottom-3 left-3 z-10 bg-background/95 backdrop-blur-md rounded-lg px-3 py-2 border border-border/50 shadow-lg">
<div className="flex items-center gap-4 text-[10px]">
<div className="flex items-center gap-1.5">
<div className="size-3 rounded-full bg-emerald-500" />
<span className="text-muted-foreground">High</span>
</div>
<div className="flex items-center gap-1.5">
<div className="size-2 rounded-full bg-emerald-500" />
<span className="text-muted-foreground">Medium</span>
</div>
<div className="flex items-center gap-1.5">
<div className="size-1.5 rounded-full bg-emerald-500" />
<span className="text-muted-foreground">Low</span>
</div>
</div>
</div>
<Map center={[0, 30]} zoom={1.5} projection={{ type: "globe" }} renderWorldCopies={false}>
{analyticsData.map((loc) => (
<MapMarker key={loc.city} longitude={loc.lng} latitude={loc.lat}>
<MarkerContent>
<div className="relative flex items-center justify-center">
<div
className="absolute rounded-full bg-emerald-500/20"
style={{
width: loc.size * 2.5,
height: loc.size * 2.5,
}}
/>
<div
className="absolute rounded-full bg-emerald-500/40 animate-ping"
style={{
width: loc.size * 1.5,
height: loc.size * 1.5,
animationDuration: "2s",
}}
/>
<div
className="relative rounded-full bg-emerald-500 shadow-lg shadow-emerald-500/50"
style={{ width: loc.size, height: loc.size }}
/>
</div>
</MarkerContent>
<MarkerTooltip>
<div className="text-center">
<div className="font-medium">{loc.city}</div>
<div className="text-emerald-500 font-semibold">
{loc.users}
</div>
<div className="text-[10px] text-muted-foreground">
active users
</div>
</div>
</MarkerTooltip>
</MapMarker>
))}
<MapControls showZoom showCompass />
</Map>
</div>
);
}
Markers
"use client";
import {
Map,
MapMarker,
MarkerContent,
MarkerTooltip,
} from "@/components/vritti/map";
import { Zap } from "lucide-react";
type Status = "available" | "in-use" | "offline";
interface ChargingStation {
name: string;
lng: number;
lat: number;
status: Status;
detail: string;
}
const stations: ChargingStation[] = [
{ name: "Union Square", lng: -122.4074, lat: 37.7879, status: "available", detail: "50 kW • $0.28/kWh" },
{ name: "Castro Station", lng: -122.435, lat: 37.7625, status: "in-use", detail: "~15 min remaining" },
{ name: "Hayes Valley", lng: -122.4264, lat: 37.7759, status: "offline", detail: "" },
{ name: "Embarcadero", lng: -122.3934, lat: 37.7935, status: "available", detail: "350 kW • $0.40/kWh" },
{ name: "Marina District", lng: -122.437, lat: 37.801, status: "available", detail: "150 kW • $0.32/kWh" },
{ name: "SoMa Charger", lng: -122.401, lat: 37.778, status: "available", detail: "50 kW • $0.30/kWh" },
{ name: "Noe Valley", lng: -122.431, lat: 37.75, status: "available", detail: "150 kW • $0.33/kWh" },
{ name: "Richmond Charger", lng: -122.478, lat: 37.781, status: "in-use", detail: "~8 min remaining" },
{ name: "Potrero Hill", lng: -122.401, lat: 37.76, status: "offline", detail: "" },
{ name: "Mission Bay", lng: -122.391, lat: 37.77, status: "available", detail: "350 kW • $0.38/kWh" },
{ name: "Golden Gate Park", lng: -122.466, lat: 37.77, status: "available", detail: "150 kW • $0.34/kWh" },
];
const statusConfig: Record<Status, { bg: string; label: string; textClass: string }> = {
available: { bg: "bg-emerald-500", label: "Available", textClass: "text-emerald-500" },
"in-use": { bg: "bg-amber-500", label: "In Use", textClass: "text-amber-500" },
offline: { bg: "bg-zinc-400", label: "Offline", textClass: "text-muted-foreground" },
};
export function MapMarkersExample() {
return (
<div className="h-[400px] w-full overflow-hidden rounded-lg border">
<Map center={[-122.434, 37.776]} zoom={12}>
{stations.map((station) => {
const config = statusConfig[station.status];
return (
<MapMarker
key={station.name}
longitude={station.lng}
latitude={station.lat}
>
<MarkerContent>
<div className={`${config.bg} rounded-full p-1.5 shadow-lg`}>
<Zap className="size-3 fill-white text-white" />
</div>
</MarkerContent>
<MarkerTooltip>
<div className="space-y-0.5 text-xs">
<div className="font-medium">{station.name}</div>
<div className="flex items-center gap-1">
<span className={`size-1.5 rounded-full ${config.bg}`} />
<span className={config.textClass}>{config.label}</span>
</div>
{station.detail && (
<div className="text-muted-foreground">
{station.detail}
</div>
)}
</div>
</MarkerTooltip>
</MapMarker>
);
})}
</Map>
</div>
);
}
Popups
"use client";
import {
Map,
MapMarker,
MarkerContent,
MarkerLabel,
MarkerPopup,
} from "@/components/vritti/map";
import { Star, Navigation, Clock, ExternalLink } from "lucide-react";
const places = [
{
id: 1,
name: "The Metropolitan Museum of Art",
label: "Museum",
category: "Museum",
rating: 4.8,
reviews: 12453,
hours: "10:00 AM - 5:00 PM",
lng: -73.9632,
lat: 40.7794,
},
{
id: 2,
name: "Brooklyn Bridge",
label: "Landmark",
category: "Landmark",
rating: 4.9,
reviews: 8234,
hours: "Open 24 hours",
lng: -73.9969,
lat: 40.7061,
},
{
id: 3,
name: "Grand Central Terminal",
label: "Transit",
category: "Transit",
rating: 4.7,
reviews: 5621,
hours: "5:15 AM - 2:00 AM",
lng: -73.9772,
lat: 40.7527,
},
];
export function MapPopupsExample() {
return (
<div className="h-[500px] w-full overflow-hidden rounded-lg border">
<Map center={[-73.98, 40.74]} zoom={11}>
{places.map((place) => (
<MapMarker key={place.id} longitude={place.lng} latitude={place.lat}>
<MarkerContent>
<div className="size-5 rounded-full bg-rose-500 border-2 border-white shadow-lg cursor-pointer hover:scale-110 transition-transform" />
<MarkerLabel position="bottom">{place.label}</MarkerLabel>
</MarkerContent>
<MarkerPopup className="p-0 w-62">
<div className="space-y-2 p-3">
<div>
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
{place.category}
</span>
<h3 className="font-semibold text-foreground leading-tight">
{place.name}
</h3>
</div>
<div className="flex items-center gap-3 text-sm">
<div className="flex items-center gap-1">
<Star className="size-3.5 fill-amber-400 text-amber-400" />
<span className="font-medium">{place.rating}</span>
<span className="text-muted-foreground">
({place.reviews.toLocaleString()})
</span>
</div>
</div>
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Clock className="size-3.5" />
<span>{place.hours}</span>
</div>
<div className="flex gap-2 pt-1">
<button className="flex-1 h-8 inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground text-sm font-medium px-3">
<Navigation className="size-3.5 mr-1.5" />
Directions
</button>
<button className="h-8 inline-flex items-center justify-center rounded-md border border-input bg-background text-sm px-2 hover:bg-accent">
<ExternalLink className="size-3.5" />
</button>
</div>
</div>
</MarkerPopup>
</MapMarker>
))}
</Map>
</div>
);
}
Route
import {
Map,
MapMarker,
MarkerContent,
MarkerTooltip,
MapRoute,
} from "@/components/vritti/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 MapRouteExample() {
return (
<div className="h-[400px] w-full overflow-hidden rounded-lg border">
<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="size-5 rounded-full bg-blue-500 border-2 border-white shadow-lg flex items-center justify-center text-white text-[10px] font-semibold">
{index + 1}
</div>
</MarkerContent>
<MarkerTooltip>{stop.name}</MarkerTooltip>
</MapMarker>
))}
</Map>
</div>
);
}
Trail
Central Park Loop
6.2
Miles
32
Mins
285
Cal
"use client";
import { Map, MapRoute, MapMarker, MarkerContent } from "@/components/vritti/map";
import { Bike, Flame, Clock, Route } from "lucide-react";
const trailCoordinates: [number, number][] = [
[-73.95846730810143, 40.80035246904919],
[-73.9717593682683, 40.78210942124929],
[-73.98192123136191, 40.76793032580281],
[-73.97393759456651, 40.76462909128966],
[-73.97291537521572, 40.765159628993814],
[-73.96920618484948, 40.7637106622374],
[-73.96383691302509, 40.77117117897504],
[-73.9584024523858, 40.76889223221369],
[-73.9470773638119, 40.784238113060894],
[-73.95585246901248, 40.78786547226602],
[-73.94937945594087, 40.79668351998197],
[-73.9498273526222, 40.797167598041455],
[-73.95699644240298, 40.80016017872583],
];
const start = trailCoordinates[0];
const end = trailCoordinates[trailCoordinates.length - 1];
export function MapTrailExample() {
return (
<div className="h-[450px] w-full relative overflow-hidden rounded-lg border">
<div className="absolute top-3 left-3 z-10 bg-background/95 backdrop-blur-md rounded-lg p-3 border border-border/50 shadow-lg">
<div className="flex items-center gap-1.5 mb-2">
<Bike className="size-3.5 text-emerald-500" />
<span className="text-xs font-medium">Central Park Loop</span>
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div>
<div className="flex items-center justify-center gap-1 text-muted-foreground mb-0.5">
<Route className="size-3" />
</div>
<div className="text-sm font-semibold">6.2</div>
<div className="text-[9px] text-muted-foreground uppercase">
Miles
</div>
</div>
<div>
<div className="flex items-center justify-center gap-1 text-muted-foreground mb-0.5">
<Clock className="size-3" />
</div>
<div className="text-sm font-semibold">32</div>
<div className="text-[9px] text-muted-foreground uppercase">
Mins
</div>
</div>
<div>
<div className="flex items-center justify-center gap-1 text-muted-foreground mb-0.5">
<Flame className="size-3" />
</div>
<div className="text-sm font-semibold">285</div>
<div className="text-[9px] text-muted-foreground uppercase">
Cal
</div>
</div>
</div>
</div>
<Map center={[-73.965, 40.782]} zoom={13}>
<MapRoute
coordinates={trailCoordinates}
color="#10b981"
width={3}
opacity={0.9}
/>
<MapMarker longitude={start[0]} latitude={start[1]}>
<MarkerContent>
<div className="size-3 rounded-full bg-emerald-500 border-2 border-white shadow-lg" />
</MarkerContent>
</MapMarker>
<MapMarker longitude={end[0]} latitude={end[1]}>
<MarkerContent>
<div className="size-3 rounded-full bg-red-500 border-2 border-white shadow-lg" />
</MarkerContent>
</MapMarker>
</Map>
</div>
);
}