web-maps-mapbox · git:20260906.d80c3e7 · 2026-09-06 · sha256 722d7df7079c733c
web-maps-mapbox git:20260906.d80c3e7A
Immutable. This exact content is served forever at /api/v1/blob/722d7df7079c733c.
---
name: web-maps-mapbox
description: Mapbox GL JS interactive vector maps. Use when building a map with sources and layers, expressions, clustering, 3D terrain, geocoding or directions.
---
# Mapbox GL JS Patterns
> **Quick Guide:** Mapbox GL JS renders vector tiles on the GPU, and the whole mental model is
> **sources hold data, layers visualize them, expressions make the visualization data-driven**. One
> source can feed several layers, and a layer's appearance is decided by a JSON expression rather
> than by JavaScript touching features. Everything that adds data waits for the style — `load` or
> `style.load` — because the map has no style at construction. Clustering is a flag on a GeoJSON
> source, not a plugin. **Current: v3**, where the Standard style is the default and custom layers
> are placed into named slots rather than before a layer id.
**Detailed Resources:**
- [examples/core.md](examples/core.md) — map setup and cleanup, markers, popups, controls, camera animation, feature-state hover, custom controls
- [examples/layers.md](examples/layers.md) — sources, every layer type, expressions, filters, clustering, safe removal
- [examples/interaction.md](examples/interaction.md) — 3D terrain, fog, fill-extrusion, heatmaps, geocoder and directions plugins, `queryRenderedFeatures`, image sources
- [reference.md](reference.md) — v3 migration, slots and configuration, layer and source tables, the full expression operator list, event table, performance tuning
---
## Which path applies
- **The Standard style** (v3's default, `mapbox://styles/mapbox/standard`) — place custom layers with
`slot: "bottom" | "middle" | "top"`, and change the basemap's own appearance through
`setConfigProperty` rather than by editing its layers.
- **A classic or custom style** — there are no slots; `addLayer(layer, beforeId)` positions a layer
relative to an existing one, which means reading `map.getStyle().layers` to find the id.
- **Terrain, fog or anything that must survive a style switch** — register it on `style.load`, which
fires again on every `setStyle`, rather than on `load`, which fires once. See
[examples/interaction.md](examples/interaction.md).
---
<critical_requirements>
## Before writing Mapbox GL JS code
**Add a source before any layer that references it.** A layer naming a source that does not exist
throws, and layers cannot be reordered around that.
**Do source and layer work inside a `load` or `style.load` handler.** The map has no style when the
constructor returns, so `addSource` immediately after `new mapboxgl.Map()` fails with "Style is not
done loading".
**Call `map.remove()` when the map goes away.** It releases the WebGL context, and browsers cap how
many can exist at once — a leaked map makes the next one fail to initialize.
**Style with expressions rather than by looping over features.** An expression runs on the GPU for
every feature at once; a JavaScript loop that sets styles individually gives up the rendering model
the library exists for.
**Put untrusted content through `setText()` or `setDOMContent()`.** `Popup.setHTML()` renders what
it is given without sanitizing it.
</critical_requirements>
---
**Auto-detection:** Mapbox, mapbox-gl, mapboxgl, `mapboxgl.Map`, `mapboxgl.Marker`, `mapboxgl.Popup`, NavigationControl, GeolocateControl, ScaleControl, addSource, addLayer, setPaintProperty, setLayoutProperty, setFilter, setFeatureState, queryRenderedFeatures, querySourceFeatures, getClusterExpansionZoom, fill-extrusion, raster-dem, setTerrain, setFog, setConfigProperty, `mapbox://styles/mapbox/standard`, `@mapbox/mapbox-gl-geocoder`, `@mapbox/mapbox-gl-directions`, `@mapbox/mapbox-gl-draw`
**Applies to:**
- Interactive vector maps with custom styling
- Point, line and polygon data styled from its own properties
- Markers, popups and map controls, including custom ones through `IControl`
- Large datasets through clustering, heatmaps and GPU-rendered layers
- 3D — terrain, fog, extruded buildings
- Camera animation and layer-scoped event handling
- The Standard style's slot system and configuration API
**Handled elsewhere:**
- Where the GeoJSON comes from — a source takes an object or a URL, and fetching, caching and paging
it are not the map's concern
- Sanitizing content before it reaches `setHTML` — the popup renders raw markup and cleans nothing
- Provisioning and restricting the access token — the map reads a token, and where it is stored and
what it is scoped to is a deployment decision
- How markers, popups and controls look — the library supplies elements and class names, and the CSS
in them is settled by whatever owns styling
- Rendering a map as a static image server-side — this is a WebGL client
---
<philosophy>
**Sources, layers, expressions**, and the separation between them is the point:
1. **Sources** hold data — GeoJSON, vector tiles, raster tiles, elevation, images
2. **Layers** decide how a source is drawn — fill, line, circle, symbol, fill-extrusion, heatmap,
raster
3. **Expressions** make a layer data-driven — colour by property, size by zoom, filter by attribute
So one GeoJSON source can be a fill layer and a line layer at once, and restyling is a change to a
layer's paint properties with the data untouched.
**Styling is declarative and runs on the GPU.** An expression is a JSON array evaluated per feature
per frame by the renderer, which is why the same expression costs the same on ten features and on a
hundred thousand.
**The style is a document the map loads**, and the Standard style in v3 is a live one: slots are the
insertion points it publishes for your layers, and `setConfigProperty` is how you change what it
draws without knowing what is inside it.
</philosophy>
---
<decision_framework>
### Which layer type
```
Points
├─ Fewer than ~100, with custom HTML? → Markers (DOM elements)
├─ Many, or styled from data? → circle layer, or symbol for icons and labels
└─ Density rather than individuals? → heatmap layer
Lines and routes → line layer
Polygons
├─ Flat areas? → fill layer
└─ Extruded? → fill-extrusion layer
Imagery → raster layer
```
### Which source type
```
GeoJSON, local or from an API → type: "geojson"; setData() to replace, cluster: true past ~500 points
A tileset or third-party tiles → type: "vector"; every layer needs "source-layer"
Elevation → type: "raster-dem", consumed by setTerrain
A georeferenced image → type: "image", with its four corner coordinates
```
### Markers or a circle layer
```
< 100, needing custom HTML or their own interaction → Markers
100 – 10,000 → circle layer
10,000+ → circle layer, cluster: true on the source
```
### Which expression
```
Same for every feature? → a literal paint value
Discrete categories? → "match"
A continuous range? → "interpolate"
Conditional logic? → "case"
Changing with zoom? → "interpolate" over ["zoom"]
Hover or selection state? → "case" over ["feature-state", ...], updated by setFeatureState
```
</decision_framework>
---
<patterns>
## Core patterns
### Pattern 1: Map initialization
```typescript
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
mapboxgl.accessToken = process.env.MAPBOX_ACCESS_TOKEN!;
const map = new mapboxgl.Map({
container: "map", // element id or the element itself
style: "mapbox://styles/mapbox/standard",
center: [-74.006, 40.7128], // [lng, lat]
zoom: 12,
});
map.on("load", () => {
// sources and layers from here
});
```
Full code, including teardown: [examples/core.md](examples/core.md)
---
### Pattern 2: Markers, popups and controls
A Marker is a DOM element pinned to a coordinate; a Popup is content anchored to one.
```typescript
const popup = new mapboxgl.Popup({ offset: 25 }).setText("Description");
new mapboxgl.Marker({ color: "#e74c3c" })
.setLngLat([-74.006, 40.7128])
.setPopup(popup)
.addTo(map);
map.addControl(new mapboxgl.NavigationControl(), "top-right");
```
Binding the popup to the marker is what makes it open and close on click without a handler.
Full code: [examples/core.md](examples/core.md)
---
### Pattern 3: Sources and layers
Add the source once; add as many layers over it as the visualization needs.
```typescript
map.on("load", () => {
map.addSource("parks", { type: "geojson", data: parksGeoJSON });
map.addLayer({
id: "parks-fill",
type: "fill",
source: "parks",
slot: "middle", // Standard style placement
paint: { "fill-color": "#2ecc71", "fill-opacity": 0.5 },
});
map.addLayer({
id: "parks-outline",
type: "line",
source: "parks",
slot: "middle",
paint: { "line-color": "#27ae60", "line-width": 2 },
});
});
```
Full code: [examples/layers.md](examples/layers.md)
---
### Pattern 4: Data-driven styling with expressions
An expression is a JSON array the renderer evaluates per feature.
```typescript
paint: {
"circle-radius": ["interpolate", ["linear"], ["get", "population"],
10_000, 5, 1_000_000, 30],
"circle-color": ["match", ["get", "type"],
"capital", "#e74c3c",
"major", "#3498db",
"#95a5a6"], // the fallback is required, and covers missing properties
}
```
Full operator list in [reference.md](reference.md). Full code:
[examples/layers.md](examples/layers.md)
---
### Pattern 5: Clustering
Clustering is a property of the source, and the three layers over it split by whether a feature is a
cluster.
```typescript
map.addSource("earthquakes", {
type: "geojson",
data: "/data/earthquakes.geojson",
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50,
});
```
The source then adds `point_count` and `cluster_id` to every cluster feature, which is what
`filter: ["has", "point_count"]` selects on.
Full code, all three layers and click-to-expand: [examples/layers.md](examples/layers.md)
---
### Pattern 6: Camera animation
```typescript
map.flyTo({ center: [-122.4194, 37.7749], zoom: 15, essential: true });
map.easeTo({ center, zoom, duration: 2000, bearing: 45, pitch: 60 });
map.fitBounds([sw, ne], { padding: 50 });
```
`flyTo` arcs, `easeTo` is a direct transition, and `fitBounds` derives the camera from data extent.
`essential: true` makes the animation ignore `prefers-reduced-motion`, so it belongs on navigation
the user asked for and nowhere else.
Full code: [examples/core.md](examples/core.md)
---
### Pattern 7: Layer-scoped events
Passing a layer id scopes the handler to features in that layer and puts them on the event.
```typescript
map.on("click", "parks-fill", (e) => {
const feature = e.features?.[0];
if (!feature) return;
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setText(feature.properties?.name ?? "")
.addTo(map);
});
map.on(
"mouseenter",
"parks-fill",
() => (map.getCanvas().style.cursor = "pointer"),
);
map.on("mouseleave", "parks-fill", () => (map.getCanvas().style.cursor = ""));
```
Without the cursor change nothing tells the user the feature is clickable.
Full code, including feature-state hover: [examples/core.md](examples/core.md)
---
### Pattern 8: 3D terrain and fog
Elevation is a `raster-dem` source consumed by `setTerrain`; fog is what makes the horizon read as
distance rather than as a cut-off.
```typescript
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.setFog({ range: [-1, 2], "horizon-blend": 0.3, color: "white" });
});
```
`style.load` rather than `load`, so terrain survives a `setStyle`.
Full code: [examples/interaction.md](examples/interaction.md)
---
### Pattern 9: Standard style configuration
Change what the basemap draws without replacing it or reaching into its layers.
```typescript
new mapboxgl.Map({
style: "mapbox://styles/mapbox/standard",
config: {
basemap: { lightPreset: "dusk", showPointOfInterestLabels: false },
},
});
map.setConfigProperty("basemap", "lightPreset", "night");
```
Full property list in [reference.md](reference.md).
</patterns>
---
<red_flags>
## Red flags
**Breaks at runtime:**
- `addSource`/`addLayer` called before the style loads — "Style is not done loading" — move them into
a `load` or `style.load` handler
- A layer naming a source that has not been added — throws — add the source first
- `map.getSource(id)` used without a guard — it returns `undefined` for an unknown id — check it, and
narrow on `source.type` before calling `setData`
- `removeSource` before `removeLayer` — a source cannot be removed while a layer references it —
remove the layers first
- No `map.remove()` on unmount — leaks the WebGL context, and browsers cap how many can exist, so a
later map silently fails to initialize
- `Popup.setHTML()` with user input — the content is rendered unsanitized — use `setText()`, or
`setDOMContent()` with elements you built
- A `fill-extrusion` layer with no `fill-extrusion-height` — the extrusions render flat, so the layer
looks like it is not working
**Surprising behaviour:**
- Coordinates are `[longitude, latitude]`, the reverse of the `[lat, lng]` order most mapping code
uses — a swapped pair lands in the wrong hemisphere rather than erroring
- `queryRenderedFeatures` only sees what is currently drawn in the viewport; `querySourceFeatures`
reaches the rest
- `setData()` replaces the entire dataset and re-parses it — for visual state per feature use
`setFeatureState` instead
- `style.load` fires on every `setStyle`; `load` fires once — anything that has to survive a style
switch belongs on the first
- An expression reading a missing property yields `null`, so `match`, `case` and `coalesce` need
their fallback branch
- `essential: true` overrides `prefers-reduced-motion` — correct for navigation, wrong for
decoration
- A clustered source writes `point_count` and `cluster_id` onto cluster features; do not add
properties by those names
- `getClusterExpansionZoom` answers through a callback, so handle its error and check the map still
exists before animating
- `slot` is ignored outside the Standard style — a classic style positions layers with `beforeId`
- Markers past a hundred or so points are DOM elements and cost like DOM elements; a circle layer
renders on the GPU
- `map.on("click", handler)` with no layer id fires on every click anywhere on the map, including
clicks the user meant for a feature
- `@types/mapbox-gl` is a deprecated stub — types ship inside `mapbox-gl` from v3
</red_flags>