Claude Code for Mapbox: Interactive Maps with React — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Mapbox: Interactive Maps with React
Frontend

Claude Code for Mapbox: Interactive Maps with React

Published: June 16, 2027
Read time: 6 min read
By: Claude Skills 360

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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free