
Mapbox Web Integration Patterns
- 1.8k installs
- 71 repo stars
- Updated August 4, 2026
- mapbox/mapbox-agent-skills
Mapbox web integration patterns.
About
The mapbox-web-integration-patterns skill documents Mapbox GL JS and Search JS integration for web apps. Covers CDN and npm installs version migration v2 to v3 framework minimums React 16.8 plus Vue Svelte Angular Next.js 13 App Router create-web-app scaffolds. Search JS packages differ for React versus other frameworks. Use embedding maps geocoding search widgets and web map patterns with correct GL JS CSS script tags and framework-specific setup for production Mapbox web integrations. Mapbox GL JS CDN and npm integration. Search JS React vs web packages. Framework mins React Vue Svelte Angular Next. create-web-app scaffolds current versions. v2 to v3 migration notes. Mapbox web integration patterns. User asks Mapbox GL JS web integration.
- Mapbox GL JS CDN and npm integration.
- Search JS React vs web packages.
- Framework mins React Vue Svelte Angular Next.
- create-web-app scaffolds current versions.
- v2 to v3 migration notes.
Mapbox Web Integration Patterns by the numbers
- 1,834 all-time installs (skills.sh)
- +102 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #262 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
mapbox-web-integration-patterns capabilities & compatibility
- Capabilities
- gl js setup · search js
- Use cases
- frontend · ui design
npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-web-integration-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 71 |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | mapbox/mapbox-agent-skills ↗ |
Embed Mapbox map in React?
Integrate Mapbox GL JS Search JS and web patterns across React Vue Svelte Angular Next frameworks.
Who is it for?
Frontend devs adding maps.
Skip if: Mobile native maps only.
When should I use this skill?
User asks Mapbox GL JS web integration.
What you get
GL JS and Search JS wired correctly.
- Framework integration code patterns
- Map lifecycle cleanup handlers
By the numbers
- Documents integration patterns across 5 web frameworks: React, Next.js, Vue, Svelte, and Angular
Files
Mapbox Integration Patterns Skill
This skill provides official patterns for integrating Mapbox GL JS into web applications using React, Vue, Svelte, Angular, and vanilla JavaScript. These patterns are based on Mapbox's create-web-app scaffolding tool and represent production-ready best practices.
Version Requirements
Mapbox GL JS
Recommended: v3.x (latest)
- Minimum: v3.0.0
- Why v3.x: Modern API, improved performance, active development
- v2.x: Legacy; no longer actively developed (see migration notes below)
Installing via npm (recommended for production):
npm install mapbox-gl@^3.0.0 # Installs latest v3.xCDN (for prototyping only):
<!-- Replace VERSION with latest v3.x from https://docs.mapbox.com/mapbox-gl-js/ -->
<script src="https://api.mapbox.com/mapbox-gl-js/vVERSION/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/vVERSION/mapbox-gl.css" rel="stylesheet" />Framework Requirements
React: GL JS works with React 16.8+ (requires hooks). create-web-app scaffolds with React 19.x. Vue: GL JS works with Vue 2.x+ (Vue 3 Composition API recommended). Svelte: GL JS works with any Svelte version. create-web-app scaffolds with Svelte 5.x. Angular: GL JS works with Angular 2+. create-web-app scaffolds with Angular 19.x. Next.js: Minimum 13.x (App Router), Pages Router 12.x+.
Mapbox Search JS
npm install @mapbox/search-js-react@^1.0.0 # React
npm install @mapbox/search-js-web@^1.0.0 # Other frameworksVersion Migration Notes (v2.x to v3.x)
- WebGL 2 now required
optimizeForTerrainoption removed- Improved TypeScript types, better tree-shaking support
- No breaking changes to core initialization patterns
Token patterns (work in v2.x and v3.x):
const token = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; // Use env vars in production
// Global token (works since v1.x)
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({ container: '...' });
// Per-map token (preferred for multi-map setups)
const map = new mapboxgl.Map({
accessToken: token,
container: '...'
});Core Principles
Every Mapbox GL JS integration must:
1. Initialize the map in the correct lifecycle hook 2. Store map instance in component state (not recreate on every render) 3. Always call `map.remove()` on cleanup to prevent memory leaks 4. Handle token management securely (environment variables) 5. Import CSS: import 'mapbox-gl/dist/mapbox-gl.css'
React Integration (Primary Pattern)
Pattern: useRef + useEffect with cleanup
Note: These examples use Vite (the bundler used increate-web-app). If using Create React App, replaceimport.meta.env.VITE_MAPBOX_ACCESS_TOKENwithprocess.env.REACT_APP_MAPBOX_TOKEN. See Token Management Patterns for other bundlers.
import { useRef, useEffect } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
function MapComponent() {
const mapRef = useRef(null); // Store map instance
const mapContainerRef = useRef(null); // Store DOM reference
useEffect(() => {
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
mapRef.current = new mapboxgl.Map({
container: mapContainerRef.current,
center: [-71.05953, 42.3629],
zoom: 13
});
// CRITICAL: Cleanup to prevent memory leaks
return () => {
mapRef.current.remove();
};
}, []); // Empty dependency array = run once on mount
return <div ref={mapContainerRef} style={{ height: '100vh' }} />;
}Key points:
- Use
useReffor both map instance and container - Initialize in
useEffectwith empty deps[] - Always return cleanup function that calls
map.remove() - Never initialize map in render (causes infinite loops)
React + Search JS
import { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import { SearchBox } from '@mapbox/search-js-react';
import 'mapbox-gl/dist/mapbox-gl.css';
const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const center = [-71.05953, 42.3629];
function MapWithSearch() {
const mapRef = useRef(null);
const mapContainerRef = useRef(null);
const [inputValue, setInputValue] = useState('');
useEffect(() => {
mapboxgl.accessToken = accessToken;
mapRef.current = new mapboxgl.Map({
container: mapContainerRef.current,
center: center,
zoom: 13
});
return () => {
mapRef.current.remove();
};
}, []);
return (
<>
<div
style={{
margin: '10px 10px 0 0',
width: 300,
right: 0,
top: 0,
position: 'absolute',
zIndex: 10
}}
>
<SearchBox
accessToken={accessToken}
map={mapRef.current}
mapboxgl={mapboxgl}
value={inputValue}
proximity={center}
onChange={(d) => setInputValue(d)}
marker
/>
</div>
<div ref={mapContainerRef} style={{ height: '100vh' }} />
</>
);
}Search JS Integration Summary
Install:
npm install @mapbox/search-js-react # React
npm install @mapbox/search-js-web # Vanilla/Vue/SvelteBoth packages include @mapbox/search-js-core as a dependency. Only install -core directly if building a custom search UI.
Key configuration options:
accessToken: Your Mapbox public tokenmap: Map instance (must be initialized first)mapboxgl: The mapboxgl library referenceproximity:[lng, lat]to bias results geographicallymarker: Boolean to show/hide result markerplaceholder: Search box placeholder text
Positioning Search Box
Absolute positioning (overlay):
<div
style={{
position: 'absolute',
top: 10,
right: 10,
zIndex: 10,
width: 300
}}
>
<SearchBox {...props} />
</div>Common positions:
- Top-right:
top: 10px, right: 10px - Top-left:
top: 10px, left: 10px - Bottom-left:
bottom: 10px, left: 10px
Common Mistakes (Critical)
Mistake 1: Forgetting to call map.remove()
// BAD - Memory leak!
useEffect(() => {
const map = new mapboxgl.Map({ ... })
// No cleanup function
}, [])
// GOOD - Proper cleanup
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove() // Cleanup
}, [])Why: Every Map instance creates WebGL contexts, event listeners, and DOM nodes. Without cleanup, these accumulate and cause memory leaks.
Mistake 2: Initializing map in render
// BAD - Infinite loop in React!
function MapComponent() {
const map = new mapboxgl.Map({ ... }) // Runs on every render
return <div />
}
// GOOD - Initialize in effect
function MapComponent() {
useEffect(() => {
const map = new mapboxgl.Map({ ... })
}, [])
return <div />
}Why: React components re-render frequently. Creating a new map on every render causes infinite loops and crashes.
Mistake 3: Not storing map instance properly
// BAD - map variable lost between renders
function MapComponent() {
useEffect(() => {
let map = new mapboxgl.Map({ ... })
// map variable is not accessible later
}, [])
}
// GOOD - Store in useRef
function MapComponent() {
const mapRef = useRef()
useEffect(() => {
mapRef.current = new mapboxgl.Map({ ... })
// mapRef.current accessible throughout component
}, [])
}Why: You need to access the map instance for operations like adding layers, markers, or calling remove().
Mistake 4: Storing map instance in Vue's data() (Vue-specific)
// BAD - Vue's reactivity wraps data() objects in a Proxy, breaking mapbox-gl internals!
export default {
data() {
return {
map: null // Will be wrapped in a Proxy
}
},
mounted() {
this.map = new mapboxgl.Map({ ... }) // Proxy breaks GL internals
}
}
// GOOD - Assign map as a plain instance property, not in data()
export default {
mounted() {
this.map = new mapboxgl.Map({
container: this.$refs.mapContainer,
center: [-71.05953, 42.3629],
zoom: 13
})
},
unmounted() {
this.map?.remove()
}
}Why: In Vue (especially Vue 3), data() properties are wrapped in a Proxy for reactivity. Mapbox GL JS internally checks object identity and uses properties that don't survive proxy wrapping. Storing the map in data() causes subtle, hard-to-debug failures. Instead, assign the map instance directly as this.map in mounted() — properties assigned outside data() are not made reactive.
Reference Files
Load these for framework-specific patterns and additional details:
references/vue.md— Vue Integration (mounted/unmounted lifecycle)references/svelte.md— Svelte Integration (onMount/onDestroy)references/angular.md— Angular Integration with SSR handlingreferences/vanilla.md— Vanilla JS (Vite) + Vanilla JS (CDN)references/web-components.md— Web Components (basic + reactive + usage in React/Vue/Svelte)references/nextjs.md— Next.js App Router + Pages Routerreferences/common-mistakes.md— Common Mistakes 4-7 + Testing Patternsreferences/token-management.md— Token Management per bundler + Style Configuration
When to Use This Skill
Invoke this skill when:
- Setting up Mapbox GL JS in a new project
- Integrating Mapbox into a specific framework (React, Vue, Svelte, Angular, Next.js)
- Building framework-agnostic Web Components
- Creating reusable map components for component libraries
- Debugging map initialization issues
- Adding Mapbox Search functionality
- Implementing proper cleanup and lifecycle management
- Converting between frameworks (e.g., React to Vue)
- Reviewing code for Mapbox integration best practices
Related Skills
- mapbox-cartography: Map design principles and styling
- mapbox-token-security: Token management and security
- mapbox-style-patterns: Common map style patterns
Resources
Mapbox Framework Integration Guide
Quick reference for integrating Mapbox GL JS with React, Vue, Svelte, Angular, and Next.js.
Critical Integration Rules
1. Map Lifecycle Management
Must properly initialize and cleanup in all frameworks:
// ✅ Initialize once, cleanup on unmount
useEffect(() => {
const map = new mapboxgl.Map({...});
return () => map.remove(); // Critical: prevents memory leaks
}, []); // Empty deps = mount once2. Never Re-initialize Map
Problem: Creating new map instances causes memory leaks Solution: Initialize once with empty dependency array
// ❌ Bad: Re-creates map on every state change
useEffect(() => {
const map = new mapboxgl.Map({...});
}, [someState]); // Re-runs on state change!
// ✅ Good: Create once, update separately
useEffect(() => {
const map = new mapboxgl.Map({...});
return () => map.remove();
}, []); // Only runs once
useEffect(() => {
if (map) map.setCenter(center);
}, [center]); // Update separately3. Wait for Map Load
All map operations must wait for 'load' event:
map.on('load', () => {
// ✅ Now safe to add sources/layers
map.addSource('data', {...});
map.addLayer({...});
});Framework-Specific Patterns
React
import { useEffect, useRef } from 'react';
function MapComponent() {
const mapContainer = useRef(null);
const map = useRef(null);
useEffect(() => {
if (map.current) return; // Initialize only once
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4, 37.8],
zoom: 12
});
return () => map.current.remove();
}, []);
// Update map when props change
useEffect(() => {
if (!map.current) return;
map.current.setCenter([lng, lat]);
}, [lng, lat]);
return <div ref={mapContainer} style={{ width: '100%', height: '400px' }} />;
}Next.js (with SSR)
'use client'; // Mark as client component
import dynamic from 'next/dynamic';
// Option 1: Dynamic import (recommended)
const Map = dynamic(() => import('./Map'), { ssr: false });
// Option 2: Check for window
useEffect(() => {
if (typeof window === 'undefined') return;
const mapboxgl = require('mapbox-gl');
// Initialize map...
}, []);Vue 3 (Composition API)
import { onMounted, onUnmounted, ref } from 'vue';
export default {
setup() {
const mapContainer = ref(null);
let map = null;
onMounted(() => {
map = new mapboxgl.Map({
container: mapContainer.value,
style: 'mapbox://styles/mapbox/streets-v12'
});
});
onUnmounted(() => {
map?.remove();
});
return { mapContainer };
}
};Svelte
<script>
import { onMount, onDestroy } from 'svelte';
import mapboxgl from 'mapbox-gl';
let mapContainer;
let map;
onMount(() => {
map = new mapboxgl.Map({
container: mapContainer,
style: 'mapbox://styles/mapbox/streets-v12'
});
});
onDestroy(() => {
map?.remove();
});
</script>
<div bind:this={mapContainer}></div>Angular
import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import mapboxgl from 'mapbox-gl';
@Component({
selector: 'app-map',
template: '<div #mapContainer></div>'
})
export class MapComponent implements OnInit, OnDestroy {
@ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef;
private map!: mapboxgl.Map;
ngOnInit() {
this.map = new mapboxgl.Map({
container: this.mapContainer.nativeElement,
style: 'mapbox://styles/mapbox/streets-v12'
});
}
ngOnDestroy() {
this.map?.remove();
}
}State Management Patterns
Updating Map from State
// ✅ Separate effects for different updates
useEffect(() => {
if (!map.current) return;
map.current.setCenter(center);
}, [center]);
useEffect(() => {
if (!map.current) return;
map.current.setZoom(zoom);
}, [zoom]);
useEffect(() => {
if (!map.current) return;
map.current.getSource('data')?.setData(geojson);
}, [geojson]);Extracting State from Map
// ✅ Update component state from map events
useEffect(() => {
if (!map.current) return;
const handleMove = () => {
setCenter(map.current.getCenter());
setZoom(map.current.getZoom());
};
map.current.on('move', handleMove);
return () => map.current.off('move', handleMove);
}, []);Common Issues
Issue: Map Not Visible
// ❌ Container has no height
<div ref={mapContainer}></div>
// ✅ Container must have explicit dimensions
<div ref={mapContainer} style={{ width: '100%', height: '400px' }}></div>Issue: Map Renders Before Container
// ❌ Map created before DOM ready
const map = new mapboxgl.Map({...}); // Too early!
// ✅ Wait for mount
useEffect(() => {
const map = new mapboxgl.Map({...}); // DOM is ready
}, []);Issue: Memory Leaks in SPA
// ❌ No cleanup
useEffect(() => {
const map = new mapboxgl.Map({...});
// Missing return statement
}, []);
// ✅ Always cleanup
useEffect(() => {
const map = new mapboxgl.Map({...});
return () => map.remove(); // Critical!
}, []);Issue: Multiple Map Instances on Same Container
// ❌ Multiple initializations — creates overlapping maps, leaks memory
useEffect(() => {
const map = new mapboxgl.Map({...});
}, [someState]); // Runs multiple times!
// ✅ Initialize once, check if exists
useEffect(() => {
if (map.current) return; // Don't re-initialize
map.current = new mapboxgl.Map({...});
}, []);SSR/Hydration Considerations
Next.js App Router
// Mark component as client-only
'use client';
// OR use dynamic import
const Map = dynamic(() => import('./Map'), { ssr: false });Next.js Pages Router
// Disable SSR for map component
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('./Map'), { ssr: false });Check for Browser Environment
useEffect(() => {
// Only runs in browser
if (typeof window === 'undefined') return;
const map = new mapboxgl.Map({...});
return () => map.remove();
}, []);Performance Tips
1. Code Splitting: Dynamic import mapbox-gl (large bundle) 2. Lazy Loading: Load map component on demand 3. Memoization: Use useMemo/React.memo for expensive computations 4. Debounce Updates: Don't update map on every keystroke
// ✅ Debounce state updates
const debouncedSearch = useMemo(() => debounce((query) => updateMap(query), 300), []);Integration Checklist
✅ Map initialized in mount hook (useEffect, onMounted, etc.) ✅ Cleanup with map.remove() in unmount hook ✅ Empty dependency array (initialize once) ✅ Container has explicit width/height ✅ Operations wait for 'load' event ✅ SSR handled (disable or check for window) ✅ State updates in separate effects ✅ Event listeners removed on cleanup ✅ No re-initialization on state changes ✅ Map reference stored (useRef, let, class property)
{
"skill_name": "mapbox-web-integration-patterns",
"evals": [
{
"id": 1,
"prompt": "Review this React Mapbox component and tell me what's wrong with it:\n\n```jsx\nimport { useState, useEffect } from 'react';\nimport mapboxgl from 'mapbox-gl';\n\nfunction MapComponent() {\n const [map, setMap] = useState(null);\n const mapContainerRef = useRef(null);\n\n useEffect(() => {\n mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;\n const newMap = new mapboxgl.Map({\n container: mapContainerRef.current,\n center: [-122.4194, 37.7749],\n zoom: 12\n });\n setMap(newMap);\n }, [map]);\n\n return <div ref={mapContainerRef} style={{ height: '100vh' }} />;\n}\n```",
"expected_output": "Should identify four bugs: (1) useState instead of useRef for map instance — storing the map in state triggers a re-render whenever setMap is called; (2) [map] in the dependency array — combined with useState, this creates an infinite loop: map is null → effect runs → setMap(newMap) → re-render → map changes → effect runs again; (3) no cleanup function returning map.remove() — causes memory leak as WebGL contexts accumulate; (4) missing CSS import 'mapbox-gl/dist/mapbox-gl.css' — map renders visually broken without it.",
"files": [],
"expectations": [
"Identifies useState should be useRef for the map instance (map doesn't need to trigger re-renders)",
"Identifies [map] dependency array causes an infinite loop when combined with setMap",
"Explains the cascading mechanism: setMap triggers re-render, map value changes, effect re-runs, creating a new map each time",
"Identifies missing cleanup: no return () => map.remove() causes memory leaks",
"Identifies missing CSS import: import 'mapbox-gl/dist/mapbox-gl.css'"
]
},
{
"id": 2,
"prompt": "Review this Vue Mapbox component and tell me what's wrong:\n\n```vue\n<template>\n <div ref=\"mapContainer\" style=\"height: 100vh;\"></div>\n</template>\n\n<script>\nimport mapboxgl from 'mapbox-gl';\n\nexport default {\n data() {\n return {\n map: null,\n };\n },\n created() {\n mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;\n this.map = new mapboxgl.Map({\n container: this.$refs.mapContainer,\n style: 'mapbox://styles/mapbox/streets-v12',\n center: [-71.05953, 42.3629],\n zoom: 13\n });\n }\n};\n</script>\n```",
"expected_output": "Should identify four issues: (1) created() should be mounted() — this.$refs.mapContainer is undefined in created() because the DOM hasn't rendered yet; (2) map should NOT be stored in data() — Vue's reactivity system wraps data() properties in a Proxy, which can interfere with Mapbox GL JS internal type checks; the correct pattern is to use this.map as a plain instance property assigned in mounted(); (3) missing unmounted() hook with this.map.remove() to prevent memory leaks; (4) missing CSS import 'mapbox-gl/dist/mapbox-gl.css'.",
"files": [],
"expectations": [
"Identifies created() should be mounted() — $refs are not available until the component is mounted",
"Identifies that map should NOT be in data() because Vue's reactivity wraps it in a Proxy",
"Recommends using this.map as a plain instance property (not in data()) following the Mapbox Vue pattern",
"Identifies missing unmounted() hook with this.map.remove() for cleanup",
"Identifies missing CSS import: import 'mapbox-gl/dist/mapbox-gl.css'"
]
},
{
"id": 3,
"prompt": "I have a React component that takes a mapStyle prop so users can switch between map styles. The map works, but every time the style changes the map completely resets — the viewport jumps back to the default position, all markers disappear, and there's a visible flash. How should I fix this?\n\n```jsx\nimport { useRef, useEffect } from 'react';\nimport mapboxgl from 'mapbox-gl';\nimport 'mapbox-gl/dist/mapbox-gl.css';\n\nfunction MapView({ mapStyle }) {\n const mapRef = useRef(null);\n const containerRef = useRef(null);\n\n useEffect(() => {\n mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;\n mapRef.current = new mapboxgl.Map({\n container: containerRef.current,\n style: mapStyle,\n center: [-122.4194, 37.7749],\n zoom: 12\n });\n return () => mapRef.current.remove();\n }, [mapStyle]);\n\n return <div ref={containerRef} style={{ height: '100vh' }} />;\n}\n```",
"expected_output": "Should explain that [mapStyle] in the useEffect dependency array causes the entire map to be destroyed and recreated from scratch on every style change — this explains the viewport reset, marker loss, and flash. The fix is the two-effect pattern: initialize the map once with an empty dependency array [], then add a separate useEffect with [mapStyle] that calls mapRef.current.setStyle(mapStyle) to update the style without destroying the map instance.",
"files": [],
"expectations": [
"Identifies that [mapStyle] in the dependency array causes the map to be fully destroyed and recreated on every style change",
"Explains why this causes the symptoms: viewport resets, markers disappear, and visible flash",
"Recommends splitting into two effects: one with [] for initialization, one with [mapStyle] for updates",
"Shows the update effect calling mapRef.current.setStyle(mapStyle) to change the style without recreating the map",
"Shows the corrected component with the two-effect pattern"
]
}
]
}
Angular Integration
Pattern: ngOnInit + ngOnDestroy with SSR handling
import { Component, ElementRef, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-map',
standalone: true,
imports: [CommonModule],
templateUrl: './map.component.html',
styleUrls: ['./map.component.scss']
})
export class MapComponent implements OnInit, OnDestroy {
@ViewChild('mapContainer', { static: false })
mapContainer!: ElementRef<HTMLDivElement>;
private map: any;
private readonly platformId = inject(PLATFORM_ID);
async ngOnInit(): Promise<void> {
// IMPORTANT: Check if running in browser (not SSR)
if (!isPlatformBrowser(this.platformId)) {
return;
}
try {
await this.initializeMap();
} catch (error) {
console.error('Failed to initialize map:', error);
}
}
private async initializeMap(): Promise<void> {
// Dynamically import to avoid SSR issues
const mapboxgl = (await import('mapbox-gl')).default;
this.map = new mapboxgl.Map({
accessToken: environment.mapboxAccessToken,
container: this.mapContainer.nativeElement,
center: [-71.05953, 42.3629],
zoom: 13
});
// Handle map errors
this.map.on('error', (e: any) => console.error('Map error:', e.error));
}
// CRITICAL: Clean up on component destroy
ngOnDestroy(): void {
if (this.map) {
this.map.remove();
}
}
}Template (map.component.html):
<div #mapContainer style="height: 100vh; width: 100%"></div>Key points:
- Use
@ViewChildto reference map container - Check `isPlatformBrowser` before initializing (SSR support)
- Dynamically import `mapbox-gl` to avoid SSR issues
- Initialize in
ngOnInit()lifecycle hook - Always implement `ngOnDestroy()` to call
map.remove() - Handle errors with
map.on('error', ...)
Common Mistakes (continued) and Testing Patterns
Mistake 4: Wrong dependency array in useEffect
// BAD - Re-creates map on every render
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove()
}) // No dependency array
// BAD - Re-creates map when props change
useEffect(() => {
const map = new mapboxgl.Map({ center: props.center, ... })
return () => map.remove()
}, [props.center])// GOOD - Initialize once
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove()
}, []) // Empty array = run once
// GOOD - Update map property instead
useEffect(() => {
if (mapRef.current) {
mapRef.current.setCenter(props.center)
}
}, [props.center])Why: Map initialization is expensive. Initialize once, then use map methods to update properties.
---
Mistake 5: Hardcoding token in source code
// BAD - Token exposed in source code
mapboxgl.accessToken = 'pk.YOUR_MAPBOX_TOKEN_HERE';// GOOD - Use environment variable
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;Why: Tokens in source code get committed to version control and exposed publicly. Always use environment variables.
---
Mistake 6: Not handling Angular SSR
// BAD - Crashes during server-side rendering
ngOnInit() {
import('mapbox-gl').then(mapboxgl => {
this.map = new mapboxgl.Map({ ... })
})
}// GOOD - Check platform first
ngOnInit() {
if (!isPlatformBrowser(this.platformId)) {
return // Skip map init during SSR
}
import('mapbox-gl').then(mapboxgl => {
this.map = new mapboxgl.Map({ ... })
})
}Why: Mapbox GL JS requires browser APIs (WebGL, Canvas). Angular Universal (SSR) will crash without platform check.
---
Mistake 7: Missing CSS import
// BAD - Map renders but looks broken
import mapboxgl from 'mapbox-gl';
// Missing CSS import// GOOD - Import CSS for proper styling
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';Why: The CSS file contains critical styles for map controls, popups, and markers. Without it, the map appears broken.
---
Testing Patterns
Unit Testing Maps
Mock mapbox-gl:
// vitest.config.js or jest.config.js
export default {
setupFiles: ['./test/setup.js']
};// test/setup.js
vi.mock('mapbox-gl', () => ({
default: {
Map: vi.fn(() => ({
on: vi.fn(),
remove: vi.fn(),
setCenter: vi.fn(),
setZoom: vi.fn()
})),
accessToken: ''
}
}));Why: Mapbox GL JS requires WebGL and browser APIs that don't exist in test environments. Mock the library to test component logic.
Next.js Specific Patterns
App Router (Recommended)
'use client' // Mark as client component
import { useRef, useEffect } from 'react'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
export default function Map() {
const mapRef = useRef<mapboxgl.Map>()
const mapContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!mapContainerRef.current) return
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!
mapRef.current = new mapboxgl.Map({
container: mapContainerRef.current,
center: [-71.05953, 42.36290],
zoom: 13
})
return () => mapRef.current?.remove()
}, [])
return <div ref={mapContainerRef} style={{ height: '100vh' }} />
}Key points:
- Must use `'use client'` directive (maps require browser APIs)
- Use
process.env.NEXT_PUBLIC_*for environment variables - Type
mapRefproperly with TypeScript
Pages Router (Legacy)
import dynamic from 'next/dynamic'
// Dynamically import to disable SSR for map component
const Map = dynamic(() => import('../components/Map'), {
ssr: false,
loading: () => <p>Loading map...</p>
})
export default function HomePage() {
return <Map />
}Key points:
- Use
dynamicimport withssr: false - Provide loading state
- Map component itself follows standard React pattern
Svelte Integration
Pattern: onMount + onDestroy
<script>
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { onMount, onDestroy } from 'svelte'
let map
let mapContainer
onMount(() => {
map = new mapboxgl.Map({
container: mapContainer,
accessToken: import.meta.env.VITE_MAPBOX_ACCESS_TOKEN,
center: [-71.05953, 42.36290],
zoom: 13
})
})
// CRITICAL: Clean up on component destroy
onDestroy(() => {
map.remove()
})
</script>
<div class="map" bind:this={mapContainer}></div>
<style>
.map {
position: absolute;
width: 100%;
height: 100%;
}
</style>Key points:
- Use
onMountfor initialization - Bind container with
bind:this={mapContainer} - Always implement `onDestroy` to call
map.remove() - Can pass
accessTokendirectly to Map constructor in Svelte
Token Management Patterns
Environment Variables (Recommended)
Different frameworks use different prefixes for client-side environment variables:
| Framework/Bundler | Environment Variable | Access Pattern |
|---|---|---|
| Vite | VITE_MAPBOX_ACCESS_TOKEN | import.meta.env.VITE_MAPBOX_ACCESS_TOKEN |
| Next.js | NEXT_PUBLIC_MAPBOX_TOKEN | process.env.NEXT_PUBLIC_MAPBOX_TOKEN |
| Create React App | REACT_APP_MAPBOX_TOKEN | process.env.REACT_APP_MAPBOX_TOKEN |
| Angular | environment.mapboxAccessToken | Environment files (environment.ts) |
Vite .env file:
VITE_MAPBOX_ACCESS_TOKEN=pk.YOUR_MAPBOX_TOKEN_HERENext.js .env.local file:
NEXT_PUBLIC_MAPBOX_TOKEN=pk.YOUR_MAPBOX_TOKEN_HEREImportant:
- Always use environment variables for tokens
- Never commit
.envfiles to version control - Use public tokens (pk.\*) for client-side apps
- Add
.envto.gitignore - Provide
.env.exampletemplate for team
.gitignore:
.env
.env.local
.env.*.local.env.example:
VITE_MAPBOX_ACCESS_TOKEN=your_token_hereStyle Configuration
Default Center and Zoom Guidelines
Example defaults (used in create-web-app demos):
- Center:
[-71.05953, 42.36290](Boston, MA) - Zoom:
13for city-level view
Note: GL JS defaults tocenter: [0, 0]andzoom: 0if not specified. Always set these explicitly.
Zoom level guide:
0-2: World view3-5: Continent/country6-9: Region/state10-12: City view13-15: Neighborhood16-18: Street level19-22: Building level
Customizing for user location:
// Use browser geolocation
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((position) => {
map.setCenter([position.coords.longitude, position.coords.latitude]);
map.setZoom(13);
});
}Vanilla JavaScript Integration
Vanilla JS with Vite
Pattern: Module imports with initialization function
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import './main.css';
// Set access token
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
let map;
/**
* Initialize the map
*/
function initMap() {
map = new mapboxgl.Map({
container: 'map-container',
center: [-71.05953, 42.3629],
zoom: 13
});
map.on('load', () => {
console.log('Map is loaded');
});
}
// Initialize when script runs
initMap();HTML:
<div id="map-container" style="height: 100vh;"></div>Key points:
- Store map in module-scoped variable
- Initialize immediately or on DOMContentLoaded
- Listen for 'load' event for post-initialization actions
---
Vanilla JS (No Bundler - CDN)
Pattern: Script tag with inline initialization
Note: This pattern is for prototyping only. Production apps should use npm/bundler for version control and offline builds.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mapbox GL JS - No Bundler</title>
<!-- Mapbox GL JS CSS -->
<!-- Replace 3.x.x with latest version from https://docs.mapbox.com/mapbox-gl-js/ -->
<link href="https://api.mapbox.com/mapbox-gl-js/v3.x.x/mapbox-gl.css" rel="stylesheet" />
<style>
body {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: 0;
padding: 0;
}
#map-container {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="map-container"></div>
<!-- Mapbox GL JS -->
<!-- Replace 3.x.x with latest version from https://docs.mapbox.com/mapbox-gl-js/ -->
<script src="https://api.mapbox.com/mapbox-gl-js/v3.x.x/mapbox-gl.js"></script>
<script>
// Set access token
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN_HERE';
let map;
function initMap() {
map = new mapboxgl.Map({
container: 'map-container',
center: [-71.05953, 42.3629],
zoom: 13
});
map.on('load', () => {
console.log('Map is loaded');
});
}
// Initialize when page loads
initMap();
</script>
</body>
</html>Key points:
- Prototyping only - not recommended for production
- Replace
3.x.xwith specific version (e.g.,3.7.0) from Mapbox docs - Don't use `/latest/` - always pin to specific version for consistency
- Initialize after script loads (bottom of body)
- For production: Use npm + bundler instead
Why not CDN for production?
- No version locking (CDN could change)
- Network dependency (breaks offline)
- Slower (no bundler optimization)
- No tree-shaking
- Use npm for production:
npm install mapbox-gl@^3.0.0
Vue Integration
Pattern: mounted + unmounted lifecycle hooks
<template>
<div ref="mapContainer" class="map-container"></div>
</template>
<script>
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
export default {
mounted() {
const map = new mapboxgl.Map({
container: this.$refs.mapContainer,
style: 'mapbox://styles/mapbox/standard',
center: [-71.05953, 42.3629],
zoom: 13
});
// Assign map instance to component property
this.map = map;
},
// CRITICAL: Clean up when component is unmounted
unmounted() {
this.map.remove();
this.map = null;
}
};
</script>
<style>
.map-container {
width: 100%;
height: 100%;
}
</style>Key points:
- Initialize in
mounted()hook - Access container via
this.$refs.mapContainer - Store map as
this.map - Always implement `unmounted()` hook to call
map.remove()
Web Components (Framework-Agnostic)
Web Components are a W3C standard for creating reusable custom elements that work in any framework or no framework at all.
When to use Web Components:
- ✅ Vanilla JavaScript apps - No framework? Web Components are a great choice
- ✅ Design systems - Building component libraries used across multiple frameworks
- ✅ Micro-frontends - Application uses different frameworks in different parts
- ✅ Multi-framework organizations - Teams working with React, Vue, Svelte, etc. need shared components
- ✅ Framework migration - Transitioning from one framework to another incrementally
- ✅ Long-term stability - W3C standard, no framework lock-in
When to use framework-specific patterns instead:
- 🔧 Already using a framework - If you're building in React, use React patterns (simpler, better integration)
- 🔧 Need framework features - Deep integration with React hooks, Vue Composition API, state management, routing
- 🔧 Team familiarity - Team is proficient with framework patterns
💡 Tip: If you're using React, Vue, Svelte, or Angular, start with the framework-specific patterns. They're simpler and better integrated. Use Web Components when you need cross-framework compatibility or are building vanilla JavaScript apps.
Real-world example: A company with React (main app), Vue (admin panel), and Svelte (marketing site) can build one <mapbox-map> component that works everywhere.
Basic Web Component
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
class MapboxMap extends HTMLElement {
constructor() {
super();
this.map = null;
}
connectedCallback() {
// Get configuration from attributes
const token = this.getAttribute('access-token') || import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const mapStyle = this.getAttribute('map-style') || 'mapbox://styles/mapbox/standard';
const center = this.getAttribute('center')?.split(',').map(Number) || [-71.05953, 42.3629];
const zoom = parseFloat(this.getAttribute('zoom')) || 13;
// Initialize map
mapboxgl.accessToken = token;
this.map = new mapboxgl.Map({
container: this,
style: mapStyle,
center: center,
zoom: zoom
});
// Dispatch custom event when map loads
this.map.on('load', () => {
this.dispatchEvent(
new CustomEvent('mapload', {
detail: { map: this.map }
})
);
});
}
// CRITICAL: Clean up when element is removed
disconnectedCallback() {
if (this.map) {
this.map.remove();
this.map = null;
}
}
// Expose map instance to JavaScript
getMap() {
return this.map;
}
}
// Register the custom element
customElements.define('mapbox-map', MapboxMap);Usage in HTML:
<!-- Basic usage -->
<mapbox-map
access-token="pk.YOUR_TOKEN"
map-style="mapbox://styles/mapbox/dark-v11"
center="-122.4194,37.7749"
zoom="12"
></mapbox-map>
<style>
mapbox-map {
display: block;
height: 100vh;
width: 100%;
}
</style>Usage in React:
import './mapbox-map-component';
function App() {
const mapRef = useRef(null);
useEffect(() => {
const handleMapLoad = (e) => {
const map = e.detail.map;
// Add markers, layers, etc.
new mapboxgl.Marker().setLngLat([-122.4194, 37.7749]).addTo(map);
};
mapRef.current?.addEventListener('mapload', handleMapLoad);
return () => {
mapRef.current?.removeEventListener('mapload', handleMapLoad);
};
}, []);
return (
<mapbox-map
ref={mapRef}
access-token={import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}
map-style="mapbox://styles/mapbox/standard"
center="-122.4194,37.7749"
zoom="12"
/>
);
}Usage in Vue:
Import the component file, then use directly in template. Vue supports custom events natively via @mapload:
<template>
<mapbox-map
ref="map"
:access-token="token"
map-style="mapbox://styles/mapbox/streets-v12"
center="-71.05953,42.3629"
zoom="13"
@mapload="handleMapLoad"
/>
</template>
<script>
import './mapbox-map-component';
export default {
data: () => ({ token: import.meta.env.VITE_MAPBOX_ACCESS_TOKEN }),
methods: {
handleMapLoad(e) {
const map = e.detail.map; /* interact */
}
}
};
</script>Usage in Svelte:
Use bind:this for element ref and on:mapload for custom events:
<script>
import './mapbox-map-component';
let mapElement;
function handleMapLoad(e) { const map = e.detail.map; /* interact */ }
</script>
<mapbox-map bind:this={mapElement} access-token={import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}
map-style="mapbox://styles/mapbox/standard" center="-71.05953,42.3629" zoom="13"
on:mapload={handleMapLoad} />Advanced: Reactive Attributes Pattern
class MapboxMapReactive extends HTMLElement {
static get observedAttributes() {
return ['center', 'zoom', 'map-style'];
}
constructor() {
super();
this.map = null;
}
connectedCallback() {
mapboxgl.accessToken = this.getAttribute('access-token');
this.map = new mapboxgl.Map({
container: this,
style: this.getAttribute('map-style') || 'mapbox://styles/mapbox/standard',
center: this.getAttribute('center')?.split(',').map(Number) || [0, 0],
zoom: parseFloat(this.getAttribute('zoom')) || 9
});
}
disconnectedCallback() {
if (this.map) {
this.map.remove();
this.map = null;
}
}
// React to attribute changes
attributeChangedCallback(name, oldValue, newValue) {
if (!this.map || oldValue === newValue) return;
switch (name) {
case 'center':
const center = newValue.split(',').map(Number);
this.map.setCenter(center);
break;
case 'zoom':
this.map.setZoom(parseFloat(newValue));
break;
case 'map-style':
this.map.setStyle(newValue);
break;
}
}
}
customElements.define('mapbox-map-reactive', MapboxMapReactive);Key points: Use connectedCallback() for init, always implement `disconnectedCallback()` with map.remove(), read config from HTML attributes, dispatch custom events (mapload), use observedAttributes + attributeChangedCallback for reactive updates. Works in any framework.
Related skills
How it compares
Pick mapbox-web-integration-patterns for web framework lifecycle fixes; pick Mapbox server or tile pipeline skills for backend geospatial data work.
FAQ
React minimum?
GL JS works React 16.8 plus hooks required.
Search JS React package?
@mapbox/search-js-react.
Next.js minimum?
13.x App Router or Pages 12.x plus.
Is Mapbox Web Integration Patterns safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.