Skip to main content
Technology & EngineeringMaps Geolocation Services258 lines

Mapbox

"Mapbox: interactive maps, GL JS, geocoding, directions, markers, layers, 3D terrain, React (react-map-gl)"

Quick Summary27 lines
Mapbox provides a GPU-accelerated, vector-tile-based mapping platform built on open standards. Mapbox GL JS renders maps client-side using WebGL, enabling smooth zooming, rotation, and 3D terrain without server round-trips for every viewport change. The platform separates data (tilesets), styling (Mapbox Studio styles), and interaction (GL JS runtime) so each layer can evolve independently. Prefer vector tiles over raster when you need dynamic styling or interactivity. Use Mapbox's hosted APIs (Geocoding, Directions, Isochrone) for server-grade spatial operations without maintaining your own PostGIS instance. In React apps, use `react-map-gl` as the canonical binding -- it wraps GL JS in a declarative component model while preserving full access to the underlying map instance when you need it.

## Key Points

- **Token security**: Use URL-restriction scoping on public tokens. Keep secret tokens server-side only for the Temporary Token API or private tilesets.
- **Viewport-driven data loading**: Fetch feature data only for the current bounding box using `map.getBounds()` to avoid loading the entire dataset upfront.
- **Style immutability**: Treat Mapbox Studio styles as versioned artifacts. Pin your style URL (e.g., `mapbox://styles/you/abc123`) rather than using mutable `streets-v12` in production.
- **Layer ordering**: Insert data layers before label layers with `map.addLayer(layerDef, "waterway-label")` so map labels remain readable.
- **Image and icon preloading**: Call `map.loadImage()` inside `style.load` before adding symbol layers that reference custom icons.
- **React strict mode**: `react-map-gl` v7+ handles double-mount correctly. Avoid wrapping GL JS instantiation in raw `useEffect` -- use the `<Map>` component instead.
- **Bundle size**: Import only `mapbox-gl` -- avoid pulling in the legacy `mapbox.js` wrapper.
- **Recreating the map on every render**: Never instantiate `new mapboxgl.Map()` inside a render function. Use `react-map-gl`'s `<Map>` component or store the instance in a ref.
- **Mutating state inside map event handlers without batching**: Map events fire at 60fps during interactions. Debounce `moveend` and `zoomend` before updating React state.
- **Embedding access tokens in source control**: Tokens pushed to public repos are scraped within minutes. Use environment variables and CI secrets.
- **Using raster tiles for interactive data**: Raster tiles cannot be queried or styled at runtime. Use vector tiles when you need click-to-inspect or dynamic theming.
- **Loading all GeoJSON features at once for large datasets**: For datasets over 10k features, use tilesets uploaded to Mapbox Studio or a server that emits vector tiles on demand.

## Quick Example

```typescript
// npm install mapbox-gl react-map-gl @types/mapbox-gl
import mapboxgl from "mapbox-gl";
import Map, { Marker, Source, Layer, NavigationControl } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
```
skilldb get maps-geolocation-services-skills/MapboxFull skill: 258 lines
Paste into your CLAUDE.md or agent config

Mapbox

Core Philosophy

Mapbox provides a GPU-accelerated, vector-tile-based mapping platform built on open standards. Mapbox GL JS renders maps client-side using WebGL, enabling smooth zooming, rotation, and 3D terrain without server round-trips for every viewport change. The platform separates data (tilesets), styling (Mapbox Studio styles), and interaction (GL JS runtime) so each layer can evolve independently. Prefer vector tiles over raster when you need dynamic styling or interactivity. Use Mapbox's hosted APIs (Geocoding, Directions, Isochrone) for server-grade spatial operations without maintaining your own PostGIS instance. In React apps, use react-map-gl as the canonical binding -- it wraps GL JS in a declarative component model while preserving full access to the underlying map instance when you need it.

Setup

Install the core libraries:

// npm install mapbox-gl react-map-gl @types/mapbox-gl
import mapboxgl from "mapbox-gl";
import Map, { Marker, Source, Layer, NavigationControl } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";

Set the access token once at the app root:

const MAPBOX_TOKEN = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!;

// Vanilla GL JS
mapboxgl.accessToken = MAPBOX_TOKEN;

// React — pass as prop
function AppMap() {
  return (
    <Map
      mapboxAccessToken={MAPBOX_TOKEN}
      initialViewState={{ longitude: -122.4, latitude: 37.8, zoom: 12 }}
      style={{ width: "100%", height: "100vh" }}
      mapStyle="mapbox://styles/mapbox/streets-v12"
    />
  );
}

Key Techniques

Geocoding with the Search API

async function forwardGeocode(query: string): Promise<[number, number] | null> {
  const url = new URL("https://api.mapbox.com/search/geocode/v6/forward");
  url.searchParams.set("q", query);
  url.searchParams.set("access_token", MAPBOX_TOKEN);
  url.searchParams.set("limit", "1");

  const res = await fetch(url);
  const data = await res.json();
  const coords = data.features?.[0]?.geometry?.coordinates;
  return coords ? [coords[0], coords[1]] : null;
}

Markers and Popups in React

import { Marker, Popup } from "react-map-gl";
import { useState } from "react";

interface Location {
  id: string;
  name: string;
  lng: number;
  lat: number;
}

function LocationMarkers({ locations }: { locations: Location[] }) {
  const [selected, setSelected] = useState<Location | null>(null);

  return (
    <>
      {locations.map((loc) => (
        <Marker
          key={loc.id}
          longitude={loc.lng}
          latitude={loc.lat}
          anchor="bottom"
          onClick={(e) => {
            e.originalEvent.stopPropagation();
            setSelected(loc);
          }}
        />
      ))}
      {selected && (
        <Popup
          longitude={selected.lng}
          latitude={selected.lat}
          anchor="top"
          onClose={() => setSelected(null)}
        >
          <h3>{selected.name}</h3>
        </Popup>
      )}
    </>
  );
}

GeoJSON Source and Layer

function RouteLayer({ geojson }: { geojson: GeoJSON.FeatureCollection }) {
  return (
    <Source id="route" type="geojson" data={geojson}>
      <Layer
        id="route-line"
        type="line"
        paint={{
          "line-color": "#3b82f6",
          "line-width": 4,
          "line-opacity": 0.8,
        }}
      />
    </Source>
  );
}

Directions API

interface DirectionsResult {
  geometry: GeoJSON.LineString;
  duration: number; // seconds
  distance: number; // meters
}

async function getDirections(
  origin: [number, number],
  destination: [number, number],
  profile: "driving" | "walking" | "cycling" = "driving"
): Promise<DirectionsResult | null> {
  const coords = `${origin[0]},${origin[1]};${destination[0]},${destination[1]}`;
  const url = `https://api.mapbox.com/directions/v5/mapbox/${profile}/${coords}?geometries=geojson&access_token=${MAPBOX_TOKEN}`;

  const res = await fetch(url);
  const data = await res.json();
  const route = data.routes?.[0];
  if (!route) return null;

  return {
    geometry: route.geometry,
    duration: route.duration,
    distance: route.distance,
  };
}

3D Terrain

import { useMap } from "react-map-gl";

function Enable3DTerrain() {
  const { current: map } = useMap();

  useEffect(() => {
    if (!map) return;
    map.on("style.load", () => {
      map.addSource("mapbox-dem", {
        type: "raster-dem",
        url: "mapbox://mapbox.mapbox-terrain-dem-v1",
        tileSize: 512,
        maxzoom: 14,
      });
      map.setTerrain({ source: "mapbox-dem", exaggeration: 1.5 });
    });
  }, [map]);

  return null;
}

Accessing the Map Instance Directly

import { useMap } from "react-map-gl";

function FlyToButton({ lng, lat }: { lng: number; lat: number }) {
  const { current: map } = useMap();

  const handleClick = () => {
    map?.flyTo({ center: [lng, lat], zoom: 14, duration: 2000 });
  };

  return <button onClick={handleClick}>Go to location</button>;
}

Clustering Points

function ClusteredPoints({ data }: { data: GeoJSON.FeatureCollection }) {
  return (
    <Source
      id="points"
      type="geojson"
      data={data}
      cluster={true}
      clusterMaxZoom={14}
      clusterRadius={50}
    >
      <Layer
        id="clusters"
        type="circle"
        filter={["has", "point_count"]}
        paint={{
          "circle-color": ["step", ["get", "point_count"], "#51bbd6", 10, "#f1f075", 50, "#f28cb1"],
          "circle-radius": ["step", ["get", "point_count"], 20, 10, 30, 50, 40],
        }}
      />
      <Layer
        id="cluster-count"
        type="symbol"
        filter={["has", "point_count"]}
        layout={{
          "text-field": ["get", "point_count_abbreviated"],
          "text-size": 12,
        }}
      />
      <Layer
        id="unclustered-point"
        type="circle"
        filter={["!", ["has", "point_count"]]}
        paint={{ "circle-color": "#11b4da", "circle-radius": 6 }}
      />
    </Source>
  );
}

Best Practices

  • Token security: Use URL-restriction scoping on public tokens. Keep secret tokens server-side only for the Temporary Token API or private tilesets.
  • Viewport-driven data loading: Fetch feature data only for the current bounding box using map.getBounds() to avoid loading the entire dataset upfront.
  • Style immutability: Treat Mapbox Studio styles as versioned artifacts. Pin your style URL (e.g., mapbox://styles/you/abc123) rather than using mutable streets-v12 in production.
  • Layer ordering: Insert data layers before label layers with map.addLayer(layerDef, "waterway-label") so map labels remain readable.
  • Image and icon preloading: Call map.loadImage() inside style.load before adding symbol layers that reference custom icons.
  • React strict mode: react-map-gl v7+ handles double-mount correctly. Avoid wrapping GL JS instantiation in raw useEffect -- use the <Map> component instead.
  • Bundle size: Import only mapbox-gl -- avoid pulling in the legacy mapbox.js wrapper.

Anti-Patterns

  • Recreating the map on every render: Never instantiate new mapboxgl.Map() inside a render function. Use react-map-gl's <Map> component or store the instance in a ref.
  • Mutating state inside map event handlers without batching: Map events fire at 60fps during interactions. Debounce moveend and zoomend before updating React state.
  • Embedding access tokens in source control: Tokens pushed to public repos are scraped within minutes. Use environment variables and CI secrets.
  • Using raster tiles for interactive data: Raster tiles cannot be queried or styled at runtime. Use vector tiles when you need click-to-inspect or dynamic theming.
  • Ignoring projection: Mapbox GL JS defaults to Web Mercator. If you render area-based analytics (e.g., choropleth density), account for Mercator distortion at high latitudes or switch to globe projection.
  • Loading all GeoJSON features at once for large datasets: For datasets over 10k features, use tilesets uploaded to Mapbox Studio or a server that emits vector tiles on demand.

Install this skill directly: skilldb add maps-geolocation-services-skills

Get CLI access →