Chapter 18 · Mapbox Web Integration Patterns
Subchapter 18.8
references/web-components.mdMarkdown6 KBView on GitHub
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:
When to use framework-specific patterns instead:
💡 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.
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} />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.