| 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
CDN (for prototyping only):
<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
npm install @mapbox/search-js-web@^1.0.0
Version Migration Notes
Migrating from v2.x to v3.x:
accessToken can 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;
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({ container: '...' });
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, replace import.meta.env.VITE_MAPBOX_ACCESS_TOKEN with process.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);
const mapContainerRef = useRef(null);
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
});
return () => {
mapRef.current.remove();
};
}, []);
return <div ref={mapContainerRef} style={{ height: '100vh' }} />;
}
Key points:
- Use
useRef for both map instance and container
- Initialize in
useEffect with 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 (
<>
<
=
' ',
,
,
,
'',
}}
>
setInputValue(d)}
marker
/>
);
}
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()
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
onMount for initialization
- Bind container with
bind:this={mapContainer}
- Always implement
onDestroy to call map.remove()
- Can pass
accessToken directly 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(): <> {
(!(.)) {
;
}
{
.();
} (error) {
.(, error);
}
}
(): <> {
mapboxgl = ( ()).;
. = mapboxgl.({
: environment.,
: ..,
: [-, ],
:
});
..(, .(, e.));
}
(): {
(.) {
..();
}
}
}
Template (map.component.html):
<div #mapContainer style="height: 100vh; width: 100%"></div>
Key points:
- Use
@ViewChild to 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', ...)
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';
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
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');
});
}
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>
<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>
</>
Key points:
- ⚠️ Prototyping only - not recommended for production
- Replace
3.x.x with 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
.env files to version control
- ✅ Use public tokens (pk.*) for client-side apps
- ✅ Add
.env to .gitignore
- ✅ Provide
.env.example template 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
npm install @mapbox/search-js-web
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';
<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 token
map: Map instance (must be initialized first)
mapboxgl: The mapboxgl library reference
proximity: [lng, lat] to bias results geographically
marker: Boolean to show/hide result marker
placeholder: 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()
useEffect(() => {
const map = new mapboxgl.Map({ ... })
}, [])
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove()
}, [])
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
function MapComponent() {
const map = new mapboxgl.Map({ ... })
return <div />
}
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
function MapComponent() {
useEffect(() => {
let map = new mapboxgl.Map({ ... })
}, [])
}
function MapComponent() {
const mapRef = useRef()
useEffect(() => {
mapRef.current = new mapboxgl.Map({ ... })
}, [])
}
Why: You need to access the map instance for operations like adding layers, markers, or calling remove().
❌ Mistake 4: Wrong dependency array in useEffect
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove()
})
useEffect(() => {
const map = new mapboxgl.Map({ center: props.center, ... })
return () => map.remove()
}, [props.center])
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove()
}, [])
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
mapboxgl.accessToken = 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGV4YW1wbGUifQ.example';
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
ngOnInit() {
import('mapbox-gl').then(mapboxgl => {
this.map = new mapboxgl.Map({ ... })
})
}
ngOnInit() {
if (!isPlatformBrowser(this.platformId)) {
return
}
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
import mapboxgl from 'mapbox-gl';
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'
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
mapRef properly with TypeScript
Pages Router (Legacy)
import dynamic from 'next/dynamic'
const Map = dynamic(() => import('../components/Map'), {
ssr: false,
loading: () => <p>Loading map...</p>
})
export default function HomePage() {
return <Map />
}
Key points:
- Use
dynamic import with ssr: 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:
13 for city-level view
Zoom level guide:
0-2: World view
3-5: Continent/country
6-9: Region/state
10-12: City view
13-15: Neighborhood
16-18: Street level
19-22: Building level
Customizing for user location:
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:
export default {
setupFiles: ['./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
Resources