dist/plugins/web-maps-mapbox/skills/web-maps-mapbox/SKILL.md
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
npx skillsauth add agents-inc/skills web-maps-mapboxInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Use Mapbox GL JS v3 for interactive vector maps. Initialize with
new mapboxgl.Map(), add data via sources (GeoJSON, vector), visualize with layers (fill, line, circle, symbol, fill-extrusion, heatmap), style dynamically with expressions. Use the Standard style as the default base with slots (bottom,middle,top) for layer placement. Enable clustering on GeoJSON sources for large point datasets. UsesetTerrain+setFogfor 3D terrain. Types are included in themapbox-glpackage (no@types/mapbox-glneeded).
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST add sources before layers that reference them -- adding a layer without its source throws a runtime error)
(You MUST listen for load or style.load before calling addSource/addLayer -- the style is not ready on construction)
(You MUST clean up map instances with map.remove() on unmount -- leaks GPU memory and event listeners)
(You MUST use named constants for coordinates, zoom levels, and style values -- NO magic numbers)
(You MUST use expressions for data-driven styling instead of iterating features and setting styles individually)
</critical_requirements>
Auto-detection: Mapbox, mapbox-gl, mapboxgl, Map, Marker, Popup, NavigationControl, GeolocateControl, addSource, addLayer, GeoJSON source, vector source, expressions, flyTo, easeTo, fitBounds, setTerrain, setFog, fill-extrusion, clustering, slot, Standard style, mapbox-gl-geocoder, mapbox-gl-directions, mapbox-gl-draw
When to use:
When NOT to use:
Key patterns covered:
Detailed Resources:
Mapbox GL JS renders vector tiles on the GPU using WebGL 2, enabling smooth 60fps map interactions with large datasets. The core mental model is sources + layers + expressions:
This separation means one source can power multiple layers (e.g., same GeoJSON rendered as both a fill layer and a line layer for borders), and layers can be styled entirely through expressions without touching the data.
v3 Standard style: The default style is mapbox://styles/mapbox/standard, which includes 3D buildings, terrain-aware rendering, and a slot system (bottom, middle, top) for inserting custom layers at predetermined positions in the visual stack. Use setConfigProperty to customize the Standard style's appearance without replacing it.
TypeScript: Types are bundled with mapbox-gl since v3 -- do not install @types/mapbox-gl.
Initialize with container, style, center, zoom. Always wait for load event before adding sources/layers.
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
const DEFAULT_CENTER: [number, number] = [-74.006, 40.7128]; // [lng, lat]
const DEFAULT_ZOOM = 12;
mapboxgl.accessToken = process.env.MAPBOX_ACCESS_TOKEN!;
const map = new mapboxgl.Map({
container: "map", // HTML element ID or element reference
style: "mapbox://styles/mapbox/standard",
center: DEFAULT_CENTER,
zoom: DEFAULT_ZOOM,
});
map.on("load", () => {
// Safe to add sources and layers here
});
Why good: Named constants for coordinates/zoom, waits for load before data operations, uses Standard style
See examples/core.md Pattern 1 for cleanup patterns and bad examples.
Markers are DOM elements placed at coordinates. Popups display content on click. Controls add navigation UI.
const MARKER_COLOR = "#e74c3c";
const popup = new mapboxgl.Popup({ offset: 25, maxWidth: "300px" })
.setHTML("<h3>Location</h3><p>Description</p>");
new mapboxgl.Marker({ color: MARKER_COLOR })
.setLngLat([-74.006, 40.7128])
.setPopup(popup)
.addTo(map);
map.addControl(new mapboxgl.NavigationControl(), "top-right");
map.addControl(new mapboxgl.GeolocateControl({ trackUserLocation: true }), "top-right");
map.addControl(new mapboxgl.ScaleControl({ unit: "metric" }), "bottom-left");
Why good: Popup bound to marker (opens on click automatically), controls positioned explicitly, named color constant
See examples/core.md Pattern 2 for custom marker elements and programmatic popup examples.
Add a GeoJSON source, then one or more layers that reference it. Sources and layers are independent -- one source can feed multiple layers.
map.on("load", () => {
map.addSource("parks", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Polygon", coordinates: [/* ... */] },
properties: { name: "Central Park", area: 3.41 },
},
],
},
});
map.addLayer({
id: "parks-fill",
type: "fill",
source: "parks",
slot: "middle", // v3 Standard style slot
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,
},
});
});
Why good: Source defined once, two layers visualize it differently, slot: "middle" places layers correctly in Standard style
See examples/layers.md Pattern 1-2 for all source types and layer configuration.
Expressions are JSON arrays that style features based on their properties or zoom level.
map.addLayer({
id: "population-circles",
type: "circle",
source: "cities",
paint: {
// Size by population
"circle-radius": [
"interpolate", ["linear"], ["get", "population"],
10000, 5,
100000, 15,
1000000, 30,
],
// Color by category
"circle-color": [
"match", ["get", "type"],
"capital", "#e74c3c",
"major", "#3498db",
"#95a5a6", // fallback
],
},
});
Why good: Expressions handle all styling on the GPU -- no JavaScript loops over features, scales with any dataset size
See examples/layers.md Pattern 3-4 for expression operators and filter expressions.
Enable clustering on a GeoJSON source for large point datasets. Use three layers: cluster circles, count labels, unclustered points.
const CLUSTER_RADIUS = 50;
const CLUSTER_MAX_ZOOM = 14;
map.addSource("earthquakes", {
type: "geojson",
data: "/data/earthquakes.geojson",
cluster: true,
clusterMaxZoom: CLUSTER_MAX_ZOOM,
clusterRadius: CLUSTER_RADIUS,
});
Why good: Clustering is handled entirely by the source -- no external library needed, automatic point_count property on clusters
See examples/layers.md Pattern 5 for complete cluster layers and click-to-expand interaction.
flyTo for dramatic transitions, easeTo for smooth pans, fitBounds for fitting data in view.
const FLY_ZOOM = 15;
const FLY_SPEED = 1.2;
const BOUNDS_PADDING_PX = 50;
map.flyTo({
center: [-122.4194, 37.7749],
zoom: FLY_ZOOM,
speed: FLY_SPEED,
essential: true, // not affected by prefers-reduced-motion
});
map.fitBounds(
[[-122.5, 37.7], [-122.3, 37.8]], // [sw, ne]
{ padding: BOUNDS_PADDING_PX },
);
Why good: essential: true ensures critical navigation animations still play even with reduced-motion preferences, padding keeps data away from edges
See examples/core.md Pattern 4 for easeTo, moveend listener, and bearing/pitch animation.
Listen for events on specific layers for interactive features (click popups, hover effects).
map.on("click", "parks-fill", (e) => {
const feature = e.features?.[0];
if (!feature) return;
const coordinates = e.lngLat;
const name = feature.properties?.name ?? "Unknown";
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(`<strong>${name}</strong>`)
.addTo(map);
});
// Cursor feedback on hover
map.on("mouseenter", "parks-fill", () => {
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "parks-fill", () => {
map.getCanvas().style.cursor = "";
});
Why good: Events scoped to a specific layer (not the whole map), cursor change signals interactivity
See examples/core.md Pattern 5 for feature-state hover highlighting.
Add elevation with a raster-dem source and atmospheric effects with fog.
const TERRAIN_EXAGGERATION = 1.5;
const TERRAIN_MAX_ZOOM = 14;
const TERRAIN_TILE_SIZE = 512;
map.on("style.load", () => {
map.addSource("mapbox-dem", {
type: "raster-dem",
url: "mapbox://mapbox.mapbox-terrain-dem-v1",
tileSize: TERRAIN_TILE_SIZE,
maxzoom: TERRAIN_MAX_ZOOM,
});
map.setTerrain({ source: "mapbox-dem", exaggeration: TERRAIN_EXAGGERATION });
map.setFog({
range: [-1, 2],
"horizon-blend": 0.3,
color: "white",
"high-color": "#add8e6",
"space-color": "#d8f2ff",
"star-intensity": 0.0,
});
});
Why good: Named exaggeration constant, terrain source separate from visual layers, fog adds atmospheric depth
See examples/interaction.md Pattern 1-2 for fog presets and fill-extrusion 3D buildings.
Customize the Standard style's built-in appearance without replacing it.
// At initialization
const map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/standard",
config: {
"basemap": {
lightPreset: "dusk",
showPointOfInterestLabels: false,
},
},
});
// At runtime
map.setConfigProperty("basemap", "lightPreset", "night");
map.setConfigProperty("basemap", "showPlaceLabels", true);
Why good: Configuration API modifies the Standard style's built-in features without needing to understand its internal layer structure
</patterns><decision_framework>
What geometry are you displaying?
|
+-> Points?
| +-> Few (<100) with custom HTML? -> Markers (DOM-based)
| +-> Many or data-driven styling? -> circle layer or symbol layer
| +-> Heatmap visualization? -> heatmap layer
|
+-> Lines/routes?
| +-> line layer (width, color, dash patterns)
|
+-> Polygons?
| +-> Flat colored areas? -> fill layer
| +-> 3D extruded shapes? -> fill-extrusion layer
|
+-> Raster imagery?
+-> raster layer (satellite, custom tiles)
Where is your data?
|
+-> Local/API GeoJSON? -> type: "geojson"
| +-> Dynamic updates? -> Use map.getSource(id).setData(newData)
| +-> Large point dataset? -> Enable cluster: true
|
+-> Mapbox tileset or third-party vector tiles? -> type: "vector"
|
+-> Elevation data? -> type: "raster-dem"
|
+-> Image overlay? -> type: "image" (with coordinates bounds)
How many points?
|
+-> < 100 with custom HTML/interaction? -> Markers (DOM elements)
+-> 100-10,000? -> circle layer (GPU-rendered)
+-> 10,000+? -> circle layer with clustering enabled on source
Is the style static (same for all features)?
|
+-> YES -> Use literal paint values: "circle-color": "#e74c3c"
+-> NO -> Does it depend on a data property?
+-> Discrete categories? -> "match" expression
+-> Continuous range? -> "interpolate" expression
+-> Conditional logic? -> "case" expression
+-> Zoom-dependent? -> "interpolate" with ["zoom"]
</decision_framework>
<red_flags>
High Priority Issues:
addSource/addLayer before the load event -- style is not ready, throws errormap.remove() on component unmount -- leaks GPU memory, WebGL contexts, and event listenersPopup.setHTML() with unsanitized user input -- XSS vulnerability. Use setText() or setDOMContent() for user dataMedium Priority Issues:
map.on("click", handler) fires for any click, map.on("click", "layer-id", handler) targets one layermouseenter/mouseleave cursor changes@types/mapbox-gl package -- types are included in mapbox-gl since v3Gotchas & Edge Cases:
[longitude, latitude] -- reversed from the common [lat, lng] order used by some librariesqueryRenderedFeatures only returns features currently visible in the viewport -- for all features use querySourceFeaturessetData() replaces the entire dataset -- for partial updates use featureState via map.setFeatureState()style.load fires every time the style changes (including setStyle), load fires only once -- use style.load for operations that must survive style switchesnull for missing properties -- always provide fallback values in match/case/coalescesetHTML does not sanitize HTML -- any user-provided content must be sanitized before passingflyTo with essential: true overrides prefers-reduced-motion -- use only for critical navigation, not decorative animationspoint_count and cluster_id properties -- do not create these manuallygetClusterExpansionZoom is async (callback-based) -- handle errors and check if map still exists before calling easeTomap.getSource() returns undefined if the source doesn't exist -- always guard the return valueslot property only works with the Standard style -- classic styles use beforeId parameter in addLayerfill-extrusion layers require fill-extrusion-height property -- without it extrusions are flat (0 height)</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST add sources before layers that reference them -- adding a layer without its source throws a runtime error)
(You MUST listen for load or style.load before calling addSource/addLayer -- the style is not ready on construction)
(You MUST clean up map instances with map.remove() on unmount -- leaks GPU memory and event listeners)
(You MUST use named constants for coordinates, zoom levels, and style values -- NO magic numbers)
(You MUST use expressions for data-driven styling instead of iterating features and setting styles individually)
Failure to follow these rules will cause runtime errors, memory leaks, and XSS vulnerabilities.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events