Awesome-omni-skill mapbox-integration-patterns
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.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/mapbox-integration-patterns" ~/.claude/skills/diegosouzapw-awesome-omni-skill-mapbox-integration-patterns && rm -rf "$T"
skills/development/mapbox-integration-patterns/SKILL.mdMapbox 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:
can now be passed to Map constructor (preferred)accessToken- 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
on cleanup to prevent memory leaksmap.remove() - 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
). If using Create React App, replacecreate-web-appwithimport.meta.env.VITE_MAPBOX_ACCESS_TOKEN. See the Token Management Patterns section for other bundlers.process.env.REACT_APP_MAPBOX_TOKEN
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
for both map instance and containeruseRef - Initialize in
with empty depsuseEffect[] - 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
hookmounted() - Access container via
this.$refs.mapContainer - Store map as
this.map - Always implement
hook to callunmounted()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
for initializationonMount - Bind container with
bind:this={mapContainer} - Always implement
to callonDestroymap.remove() - Can pass
directly to Map constructor in SvelteaccessToken
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
to reference map container@ViewChild - Check
before initializing (SSR support)isPlatformBrowser - Dynamically import
to avoid SSR issuesmapbox-gl - Initialize in
lifecycle hookngOnInit() - Always implement
to callngOnDestroy()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'; // 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
with specific version (e.g.,3.x.x
) from Mapbox docs3.7.0 - Don't use
- always pin to specific version for consistency/latest/ - 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 | | |
| Next.js | | |
| Create React App | | |
| Angular | | Environment files () |
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
files to version control.env - ✅ Use public tokens (pk.*) for client-side apps
- ✅ Add
to.env.gitignore - ✅ Provide
template for team.env.example
.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:
: Your Mapbox public tokenaccessToken
: Map instance (must be initialized first)map
: The mapboxgl library referencemapboxgl
:proximity
to bias results geographically[lng, lat]
: Boolean to show/hide result markermarker
: Search box placeholder textplaceholder
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
directive (maps require browser APIs)'use client' - Use
for environment variablesprocess.env.NEXT_PUBLIC_* - Type
properly with TypeScriptmapRef
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
import withdynamicssr: false - Provide loading state
- Map component itself follows standard React pattern
Style Configuration
Default Center and Zoom Guidelines
Recommended defaults:
- Center:
(Boston, MA) - Mapbox HQ[-71.05953, 42.36290] - Zoom:
for city-level view13
Zoom level guide:
: World view0-2
: Continent/country3-5
: Region/state6-9
: City view10-12
: Neighborhood13-15
: Street level16-18
: Building level19-22
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