| name | mapbox-integration-patterns |
| description | Official integration patterns for Mapbox GL JS across popular web frameworks. Covers setup, lifecycle management, token handling, search integration, and common pitfalls. Based on Mapbox's create-web-app scaffolding tool. |
Mapbox Integration Patterns Skill
This skill provides official patterns for integrating Mapbox GL JS into web applications across different frameworks. 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: Still supported but deprecated patterns (see migration notes below)
Installing via npm (recommended for production):
npm install mapbox-gl@^3.0.0 # Installs latest v3.x
CDN (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"
/>
⚠️ Production apps should use npm, not CDN - ensures consistent versions and offline builds.
Framework Requirements
React:
- Minimum: 19+ (current implementation in create-web-app)
- Recommended: Latest 19.x
Vue:
- Minimum: 3.x (Composition API recommended)
- Vue 2.x: Use Options API pattern (mounted/unmounted hooks)
Svelte:
- Minimum: 5+ (current implementation in create-web-app)
- Recommended: Latest 5.x
Angular:
- Minimum: 19+ (current implementation in create-web-app)
- Recommended: Latest 19.x
Next.js:
- Minimum: 13.x (App Router)
- Pages Router: 12.x+
Mapbox Search JS
Required for search integration:
npm install @mapbox/search-js-react@^1.0.0 # React
npm install @mapbox/search-js-web@^1.0.0 # Other frameworks
Version Migration Notes
Migrating from v2.x to v3.x:
accessTokencan now be passed to Map constructor (preferred)- Improved TypeScript types
- Better tree-shaking support
- No breaking changes to core initialization patterns
Example:
const token = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; // Use env vars in production
// v2.x pattern (still works in v3.x)
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({ container: '...' });
// v3.x pattern (preferred)
const map = new mapboxgl.Map({
accessToken: token,
container: '...'
});
Core Principles
Every Mapbox GL JS integration must:
- Initialize the map in the correct lifecycle hook
- Store map instance in component state (not recreate on every render)
- Always call
map.remove()on cleanup to prevent memory leaks - Handle token management securely (environment variables)
- Import CSS:
import 'mapbox-gl/dist/mapbox-gl.css'
Framework-Specific Patterns
React Integration
Pattern: useRef + useEffect with cleanup
Note: These examples use Vite (the bundler used in
create-web-app). If using Create React App, replaceimport.meta.env.VITE_MAPBOX_ACCESS_TOKENwithprocess.env.REACT_APP_MAPBOX_TOKEN. See the Token Management Patterns section 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' }} />
</>
);
}
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 callmap.remove()
Svelte Integration
Pattern: onMount + onDestroy
<script>
import { Map } from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { onMount, onDestroy } from 'svelte'
let map
let mapContainer
onMount(() => {
map = new 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
onDestroyto callmap.remove() - Can pass
accessTokendirectly to Map constructor in Svelte
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
isPlatformBrowserbefore initializing (SSR support) - Dynamically import
mapbox-glto avoid SSR issues - Initialize in
ngOnInit()lifecycle hook - Always implement
ngOnDestroy()to callmap.remove() - Handle errors with
map.on('error', ...)
Vanilla JavaScript (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 JavaScript (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?
- ❌ Network dependency (breaks offline)
- ❌ No version locking (CDN could change)
- ❌ Slower (no bundler optimization)
- ❌ No tree-shaking
- ✅ Use npm for production:
npm install mapbox-gl@^3.0.0
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.eyJ1...
Next.js .env.local file:
NEXT_PUBLIC_MAPBOX_TOKEN=pk.eyJ1...
Important:
- ✅ 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_here
Mapbox Search JS Integration
Search Box Component Pattern
Install dependency:
npm install @mapbox/search-js-react # React
npm install @mapbox/search-js-web # Vanilla/Vue/Svelte
Note: Both packages include @mapbox/search-js-core as a dependency. You only need to install -core directly if building a custom search UI.
React Search Pattern:
import { SearchBox } from '@mapbox/search-js-react';
// Inside component:
<SearchBox
accessToken={accessToken}
map={mapRef.current} // Pass map instance
mapboxgl={mapboxgl} // Pass mapboxgl library
value={inputValue}
onChange={(value) => setInputValue(value)}
proximity={centerCoordinates} // Bias results near center
marker // Show marker for selected result
/>;
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 to Avoid
❌ 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: 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.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGV4YW1wbGUifQ.example';
// 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.
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
Style Configuration
Default Center and Zoom Guidelines
Recommended defaults:
- Center:
[-71.05953, 42.36290](Boston, MA) - Mapbox HQ - Zoom:
13for city-level view
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);
});
}
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.
When to Use This Skill
Invoke this skill when:
- Setting up Mapbox GL JS in a new project
- Integrating Mapbox into a specific framework
- 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