Advanced
Access the underlying MapLibre GL instance for advanced customization.
Access the underlying MapLibre GL map instance to use any feature from the MapLibre GL JS API. You can use either a ref or the useMap hook.
Using a Ref
The simplest way to access the map instance. Use a ref to call map methods from event handlers or effects.
import { Map, type MapRef } from "@/components/ui/map";
import { useRef } from "react";
function MyMapComponent() {
const mapRef = useRef<MapRef>(null);
const handleFlyTo = () => {
// Access the MapLibre GL map instance via ref
mapRef.current?.flyTo({ center: [-74, 40.7], zoom: 12 });
};
return (
<>
<button onClick={handleFlyTo}>Fly to NYC</button>
<Map ref={mapRef} center={[-74, 40.7]} zoom={10} />
</>
);
}Using the Hook
For child components rendered inside Map, use the useMap hook to access the map instance and listen to events.
import { Map, useMap } from "@/components/ui/map";
import { useEffect } from "react";
// For child components inside Map, use the useMap hook
function MapEventListener() {
const { map, isLoaded } = useMap();
useEffect(() => {
if (!map || !isLoaded) return;
const handleClick = (e) => {
console.log("Clicked at:", e.lngLat);
};
map.on("click", handleClick);
return () => map.off("click", handleClick);
}, [map, isLoaded]);
return null;
}
// Usage
<Map center={[-74, 40.7]} zoom={10}>
<MapEventListener />
</Map>Example: Custom Controls
This example shows how to create custom controls that manipulate the map's pitch and bearing, and listen to map events to display real-time values.
"use client";
import { useEffect, useState } from "react";
import { Map, useMap } from "@/components/ui/map";
import { Button } from "@/components/ui/button";
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 size="sm" variant="secondary" onClick={handle3DView}>
<Mountain className="mr-1.5 size-4" />
3D View
</Button>
<Button size="sm" variant="secondary" onClick={handleReset}>
<RotateCcw className="mr-1.5 size-4" />
Reset
</Button>
</div>
<div className="bg-background/90 rounded-md border px-3 py-2 font-mono text-xs backdrop-blur">
<div>Pitch: {pitch}°</div>
<div>Bearing: {bearing}°</div>
</div>
</div>
);
}
export function AdvancedUsageExample() {
return (
<div className="h-[420px] w-full">
<Map center={[-73.9857, 40.7484]} zoom={15}>
<MapController />
</Map>
</div>
);
}
Example: Custom GeoJSON Layer
Add custom GeoJSON data as layers with fill and outline styles. This example shows NYC parks with hover interactions.
"use client";
import { useCallback, useEffect, useState } from "react";
import { Map, MapControls, useMap } from "@/components/ui/map";
import { Button } from "@/components/ui/button";
import { Layers, X } from "lucide-react";
const geojsonData = {
type: "FeatureCollection" as const,
features: [
{
type: "Feature" as const,
properties: { name: "Central Park", type: "park" },
geometry: {
type: "Polygon" as const,
coordinates: [
[
[-73.9731, 40.7644],
[-73.9819, 40.7681],
[-73.958, 40.8006],
[-73.9493, 40.7969],
[-73.9731, 40.7644],
],
],
},
},
{
type: "Feature" as const,
properties: { name: "Bryant Park", type: "park" },
geometry: {
type: "Polygon" as const,
coordinates: [
[
[-73.9837, 40.7536],
[-73.9854, 40.7542],
[-73.984, 40.7559],
[-73.9823, 40.7553],
[-73.9837, 40.7536],
],
],
},
},
],
};
function CustomLayer() {
const { map, isLoaded } = useMap();
const [isLayerVisible, setIsLayerVisible] = useState(false);
const [hoveredPark, setHoveredPark] = useState<string | null>(null);
const addLayers = useCallback(() => {
if (!map) return;
// Add source if it doesn't exist
if (!map.getSource("parks")) {
map.addSource("parks", {
type: "geojson",
data: geojsonData,
});
}
// Add fill layer if it doesn't exist
if (!map.getLayer("parks-fill")) {
map.addLayer({
id: "parks-fill",
type: "fill",
source: "parks",
paint: {
"fill-color": "#22c55e",
"fill-opacity": 0.4,
},
layout: {
visibility: isLayerVisible ? "visible" : "none",
},
});
}
// Add outline layer if it doesn't exist
if (!map.getLayer("parks-outline")) {
map.addLayer({
id: "parks-outline",
type: "line",
source: "parks",
paint: {
"line-color": "#16a34a",
"line-width": 2,
},
layout: {
visibility: isLayerVisible ? "visible" : "none",
},
});
}
}, [map, isLayerVisible]);
useEffect(() => {
if (!map || !isLoaded) return;
// Add layers on mount
addLayers();
// Hover effect
const handleMouseEnter = () => {
map.getCanvas().style.cursor = "pointer";
};
const handleMouseLeave = () => {
map.getCanvas().style.cursor = "";
setHoveredPark(null);
};
const handleMouseMove = (e: maplibregl.MapMouseEvent) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ["parks-fill"],
});
if (features.length > 0) {
setHoveredPark(features[0].properties?.name || null);
}
};
map.on("mouseenter", "parks-fill", handleMouseEnter);
map.on("mouseleave", "parks-fill", handleMouseLeave);
map.on("mousemove", "parks-fill", handleMouseMove);
return () => {
map.off("mouseenter", "parks-fill", handleMouseEnter);
map.off("mouseleave", "parks-fill", handleMouseLeave);
map.off("mousemove", "parks-fill", handleMouseMove);
};
}, [map, isLoaded, isLayerVisible]);
const toggleLayer = () => {
if (!map) return;
const visibility = isLayerVisible ? "none" : "visible";
map.setLayoutProperty("parks-fill", "visibility", visibility);
map.setLayoutProperty("parks-outline", "visibility", visibility);
setIsLayerVisible(!isLayerVisible);
};
return (
<>
<div className="absolute top-3 left-3 z-10">
<Button
size="sm"
variant={isLayerVisible ? "default" : "secondary"}
onClick={toggleLayer}
>
{isLayerVisible ? (
<X className="mr-1.5 size-4" />
) : (
<Layers className="mr-1.5 size-4" />
)}
{isLayerVisible ? "Hide Parks" : "Show Parks"}
</Button>
</div>
{hoveredPark && (
<div className="bg-background/90 absolute bottom-3 left-3 z-10 rounded-md border px-3 py-2 text-sm font-medium backdrop-blur">
{hoveredPark}
</div>
)}
</>
);
}
export function CustomLayerExample() {
return (
<div className="h-[420px] w-full">
<Map center={[-73.97, 40.78]} zoom={11.8}>
<MapControls />
<CustomLayer />
</Map>
</div>
);
}
Example: Markers via Layers
When displaying hundreds or thousands of markers, use GeoJSON layers instead of DOM-based MapMarker components. This approach renders markers on the WebGL canvas, providing significantly better performance.
"use client";
import { useEffect, useState, useId } from "react";
import { Map, MapPopup, useMap } from "@/components/ui/map";
// Generate random points around NYC
function generateRandomPoints(count: number) {
const center = { lng: -73.98, lat: 40.75 };
const features = [];
for (let i = 0; i < count; i++) {
const lng = center.lng + (Math.random() - 0.5) * 0.15;
const lat = center.lat + (Math.random() - 0.5) * 0.1;
features.push({
type: "Feature" as const,
properties: {
id: i,
name: `Location ${i + 1}`,
category: ["Restaurant", "Cafe", "Bar", "Shop"][
Math.floor(Math.random() * 4)
],
},
geometry: {
type: "Point" as const,
coordinates: [lng, lat],
},
});
}
return {
type: "FeatureCollection" as const,
features,
};
}
// 200 markers - would be slow with DOM markers, but fast with layers
const pointsData = generateRandomPoints(200);
interface SelectedPoint {
id: number;
name: string;
category: string;
coordinates: [number, number];
}
function MarkersLayer() {
const { map, isLoaded } = useMap();
const id = useId();
const sourceId = `markers-source-${id}`;
const layerId = `markers-layer-${id}`;
const [selectedPoint, setSelectedPoint] = useState<SelectedPoint | null>(
null,
);
useEffect(() => {
if (!map || !isLoaded) return;
map.addSource(sourceId, {
type: "geojson",
data: pointsData,
});
map.addLayer({
id: layerId,
type: "circle",
source: sourceId,
paint: {
"circle-radius": 6,
"circle-color": "#3b82f6",
"circle-stroke-width": 2,
"circle-stroke-color": "#ffffff",
// add more paint properties here to customize the appearance of the markers
},
});
const handleClick = (
e: maplibregl.MapMouseEvent & {
features?: maplibregl.MapGeoJSONFeature[];
},
) => {
if (!e.features?.length) return;
const feature = e.features[0];
const coords = (feature.geometry as GeoJSON.Point).coordinates as [
number,
number,
];
setSelectedPoint({
id: feature.properties?.id,
name: feature.properties?.name,
category: feature.properties?.category,
coordinates: coords,
});
};
const handleMouseEnter = () => {
map.getCanvas().style.cursor = "pointer";
};
const handleMouseLeave = () => {
map.getCanvas().style.cursor = "";
};
map.on("click", layerId, handleClick);
map.on("mouseenter", layerId, handleMouseEnter);
map.on("mouseleave", layerId, handleMouseLeave);
return () => {
map.off("click", layerId, handleClick);
map.off("mouseenter", layerId, handleMouseEnter);
map.off("mouseleave", layerId, handleMouseLeave);
try {
if (map.getLayer(layerId)) map.removeLayer(layerId);
if (map.getSource(sourceId)) map.removeSource(sourceId);
} catch {
// ignore cleanup errors
}
};
}, [map, isLoaded, sourceId, layerId]);
return (
<>
{selectedPoint && (
<MapPopup
longitude={selectedPoint.coordinates[0]}
latitude={selectedPoint.coordinates[1]}
onClose={() => setSelectedPoint(null)}
closeOnClick={false}
focusAfterOpen={false}
offset={10}
closeButton
>
<div className="min-w-24">
<p className="font-medium">{selectedPoint.name}</p>
<p className="text-muted-foreground text-sm">
{selectedPoint.category}
</p>
</div>
</MapPopup>
)}
</>
);
}
export function LayerMarkersExample() {
return (
<div className="h-[420px] w-full">
<Map center={[-73.98, 40.75]} zoom={11}>
<MarkersLayer />
</Map>
</div>
);
}
Extend to Build
You can extend this to build custom features like:
- Real-time tracking - Live location updates for delivery, rides, or fleet management
- Geofencing - Trigger actions when users enter or leave specific areas
- Heatmaps - Visualize density data like population, crime, or activity hotspots
- Drawing tools - Let users draw polygons, lines, or place markers for custom areas
- 3D buildings - Extrude building footprints for urban visualization
- Animations - Animate markers along routes or create fly-through experiences
- Custom data layers - Overlay weather, traffic, or satellite imagery