Mapbox GL JS with react-map-gl provides interactive maps in React — <Map mapboxAccessToken={token} mapStyle="mapbox://styles/mapbox/streets-v12"> renders the map. <Marker latitude={lat} longitude={lng}> places markers. <Popup> shows info callouts. useMap() hook returns the map instance for flyTo, fitBounds, and queryRenderedFeatures. GeoJSON source: <Source type="geojson" data={geojsonData}><Layer type="circle" paint={{ "circle-radius": 8 }} /></Source>. Clustering: <Source cluster={true} clusterRadius={50}>. Heatmap: <Layer type="heatmap" paint={{ "heatmap-intensity": [...] }} />. @mapbox/mapbox-gl-geocoder adds address search. MapboxDraw adds polygon/line drawing. map.on("click", "layer-id", handler) handles layer clicks. map.getSource("id")?.setData(newGeoJSON) updates data live. NavigationControl, ScaleControl, and GeolocateControl add UI controls. Claude Code generates Mapbox maps, cluster visualizations, heatmaps, and geocoder search.
CLAUDE.md for Mapbox
## Mapbox Stack
- Version: react-map-gl >= 7.1, mapbox-gl >= 3.6
- Token: process.env.NEXT_PUBLIC_MAPBOX_TOKEN — required for all map requests
- Map: <Map mapboxAccessToken={token} initialViewState={{ longitude, latitude, zoom: 11 }} mapStyle="mapbox://styles/mapbox/dark-v11" style={{ width: "100%", height: "100%" }}>
- Marker: <Marker longitude={lng} latitude={lat} onClick={() => setPopup(marker)}><div className="size-4 rounded-full bg-primary" /></Marker>
- Popup: <Popup longitude={lng} latitude={lat} onClose={() => setPopup(null)}><p>Content</p></Popup>
- Source+Layer: <Source type="geojson" data={geoJSON}><Layer type="circle" paint={{"circle-color": "#6366f1"}} /></Source>
- Camera: const { current: map } = useMap(); map?.flyTo({ center: [lng, lat], zoom: 14, duration: 1500 })
Map Component
// components/map/LocationMap.tsx — Mapbox map with markers and popups
"use client"
import Map, {
Marker,
Popup,
NavigationControl,
GeolocateControl,
ScaleControl,
useMap,
type MapRef,
} from "react-map-gl"
import { useRef, useState, useCallback } from "react"
import "mapbox-gl/dist/mapbox-gl.css"
type Location = {
id: string
name: string
latitude: number
longitude: number
category: string
description?: string
}
interface LocationMapProps {
locations: Location[]
center?: [number, number]
zoom?: number
onLocationClick?: (location: Location) => void
}
const CATEGORY_COLORS: Record<string, string> = {
restaurant: "#ef4444",
shop: "#3b82f6",
hotel: "#8b5cf6",
park: "#22c55e",
default: "#6366f1",
}
export function LocationMap({
locations,
center = [-74.006, 40.7128], // NYC default
zoom = 12,
onLocationClick,
}: LocationMapProps) {
const mapRef = useRef<MapRef | null>(null)
const [selectedLocation, setSelectedLocation] = useState<Location | null>(null)
const [viewState, setViewState] = useState({
longitude: center[0],
latitude: center[1],
zoom,
})
const handleMarkerClick = useCallback((location: Location) => {
setSelectedLocation(location)
onLocationClick?.(location)
// Fly to selected location
mapRef.current?.flyTo({
center: [location.longitude, location.latitude],
zoom: 15,
duration: 800,
})
}, [onLocationClick])
const fitAllLocations = useCallback(() => {
if (locations.length === 0 || !mapRef.current) return
const lngs = locations.map(l => l.longitude)
const lats = locations.map(l => l.latitude)
mapRef.current.fitBounds(
[[Math.min(...lngs), Math.min(...lats)], [Math.max(...lngs), Math.max(...lats)]],
{ padding: 60, duration: 1000 },
)
}, [locations])
return (
<div className="relative w-full h-full">
<Map
ref={mapRef}
{...viewState}
onMove={(evt) => setViewState(evt.viewState)}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/light-v11"
style={{ width: "100%", height: "100%" }}
attributionControl={false}
reuseMaps
>
{/* Controls */}
<NavigationControl position="top-right" />
<GeolocateControl
position="top-right"
trackUserLocation
onGeolocate={(e) => {
setViewState(v => ({
...v,
longitude: e.coords.longitude,
latitude: e.coords.latitude,
zoom: 14,
}))
}}
/>
<ScaleControl position="bottom-left" />
{/* Markers */}
{locations.map((location) => (
<Marker
key={location.id}
longitude={location.longitude}
latitude={location.latitude}
anchor="bottom"
onClick={(e) => {
e.originalEvent.stopPropagation()
handleMarkerClick(location)
}}
>
<div
className="cursor-pointer transition-transform hover:scale-110 active:scale-95"
style={{ color: CATEGORY_COLORS[location.category] ?? CATEGORY_COLORS.default }}
>
<div className="size-4 rounded-full border-2 border-white shadow-md"
style={{ backgroundColor: CATEGORY_COLORS[location.category] ?? CATEGORY_COLORS.default }}
/>
</div>
</Marker>
))}
{/* Popup */}
{selectedLocation && (
<Popup
longitude={selectedLocation.longitude}
latitude={selectedLocation.latitude}
anchor="bottom"
offset={16}
onClose={() => setSelectedLocation(null)}
closeButton
closeOnClick={false}
className="rounded-xl"
>
<div className="p-2 min-w-[200px]">
<p className="font-semibold text-sm">{selectedLocation.name}</p>
<p className="text-xs text-muted-foreground capitalize mt-0.5">{selectedLocation.category}</p>
{selectedLocation.description && (
<p className="text-xs mt-1.5 text-foreground/80">{selectedLocation.description}</p>
)}
</div>
</Popup>
)}
</Map>
{/* Fit all button */}
{locations.length > 1 && (
<button
onClick={fitAllLocations}
className="absolute bottom-10 right-3 bg-white border shadow-md rounded-lg px-3 py-1.5 text-xs font-medium hover:bg-gray-50 transition-colors"
>
Fit all
</button>
)}
</div>
)
}
Cluster Map
// components/map/ClusterMap.tsx — GeoJSON clustering with Mapbox
"use client"
import Map, { Source, Layer, useMap } from "react-map-gl"
import { useCallback } from "react"
import type { GeoJSON } from "geojson"
import type { CircleLayer, SymbolLayer } from "react-map-gl"
const clusterLayer: CircleLayer = {
id: "clusters",
type: "circle",
source: "locations",
filter: ["has", "point_count"],
paint: {
"circle-color": ["step", ["get", "point_count"], "#51bbd6", 10, "#f1f075", 30, "#f28cb1"],
"circle-radius": ["step", ["get", "point_count"], 20, 10, 30, 30, 40],
"circle-stroke-width": 2,
"circle-stroke-color": "#fff",
"circle-stroke-opacity": 0.8,
},
}
const clusterCountLayer: SymbolLayer = {
id: "cluster-count",
type: "symbol",
source: "locations",
filter: ["has", "point_count"],
layout: {
"text-field": "{point_count_abbreviated}",
"text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
"text-size": 12,
},
paint: { "text-color": "#fff" },
}
const unclusteredPointLayer: CircleLayer = {
id: "unclustered-point",
type: "circle",
source: "locations",
filter: ["!", ["has", "point_count"]],
paint: {
"circle-color": "#6366f1",
"circle-radius": 8,
"circle-stroke-width": 2,
"circle-stroke-color": "#fff",
},
}
interface ClusterMapProps {
geojson: GeoJSON.FeatureCollection
}
export function ClusterMap({ geojson }: ClusterMapProps) {
const { current: map } = useMap()
const onClusterClick = useCallback((e: any) => {
const features = map?.queryRenderedFeatures(e.point, { layers: ["clusters"] })
if (!features?.length) return
const clusterId = features[0].properties?.cluster_id
const source = map?.getSource("locations") as any
source?.getClusterExpansionZoom(clusterId, (err: any, zoom: number) => {
if (err) return
const coords = (features[0].geometry as any).coordinates
map?.flyTo({ center: coords, zoom, duration: 500 })
})
}, [map])
return (
<Map
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
initialViewState={{ longitude: -98.5, latitude: 39.8, zoom: 3 }}
mapStyle="mapbox://styles/mapbox/light-v11"
style={{ width: "100%", height: "100%" }}
interactiveLayerIds={["clusters"]}
onClick={onClusterClick}
>
<Source
id="locations"
type="geojson"
data={geojson}
cluster={true}
clusterMaxZoom={14}
clusterRadius={50}
>
<Layer {...clusterLayer} />
<Layer {...clusterCountLayer} />
<Layer {...unclusteredPointLayer} />
</Source>
</Map>
)
}
For the Leaflet (react-leaflet) alternative when a fully open-source, free-to-use mapping library without API key requirements, OpenStreetMap tile support, and a lighter bundle for simple marker maps is needed — Leaflet is the most common open-source option while Mapbox has superior vector tiles, 3D terrain, and a better developer experience, see the Leaflet guide. For the Google Maps alternative when Google’s vast POI database, Street View embedding, advanced places autocomplete, directions API, or maximum brand recognition on a consumer-facing product is needed — Google Maps has the most comprehensive data while Mapbox gives more design control, see the Google Maps guide. The Claude Skills 360 bundle includes Mapbox skill sets covering markers, clustering, and layers. Start with the free tier to try interactive map generation.