INTEGRATION
Element Editor with Mapbox GL JS
A modern alternative to Mapbox GL Draw — declarative, React-first, with typed schemas and a data table.
Install
npm install @element-editor/adapter-maplibre @element-editor/ui maplibre-gl mapbox-glElement uses the adapter-maplibre package for Mapbox GL JS. Mapbox GL JS v3+ and MapLibre GL JS share enough API surface at runtime that one adapter covers both. Keep maplibre-gl installed: the adapter imports it itself.
Usage
import { useEffect, useRef, useState } from 'react'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { ElementEditorUIProvider, GeoStoreProvider, ElementEditorControls } from '@element-editor/adapter-maplibre'
import '@element-editor/adapter-maplibre/style.css'
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!
export default function MapEditor() {
const container = useRef<HTMLDivElement>(null)
const mapRef = useRef<mapboxgl.Map | null>(null)
const [ready, setReady] = useState(false)
useEffect(() => {
const map = new mapboxgl.Map({
container: container.current!,
style: 'mapbox://styles/mapbox/streets-v12',
center: [2.35, 48.86],
zoom: 12,
})
mapRef.current = map
map.on('load', () => setReady(true))
return () => {
map.remove()
}
}, [])
return (
<ElementEditorUIProvider>
<GeoStoreProvider>
<div data-elm-theme="light" style={{ position: 'relative', height: 600 }}>
<div ref={container} style={{ width: '100%', height: '100%' }} />
{ready && mapRef.current && (
<ElementEditorControls
map={mapRef.current as any} // Mapbox and MapLibre types differ; same API at runtime
onChange={(data) => console.log(data)}
/>
)}
</div>
</GeoStoreProvider>
</ElementEditorUIProvider>
)
}Same pattern as MapLibre: wait for map.on('load', ...), pass the map instance via the map prop on ElementEditorControls. The only difference is the as any cast. ElementEditorUIProvider, GeoStoreProvider and the data-elm-theme element are required, as with every adapter.
Migrating from Mapbox GL Draw
Before — Mapbox GL Draw:
import MapboxDraw from '@mapbox/mapbox-gl-draw'
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
const draw = new MapboxDraw({
displayControlsDefault: false,
controls: { polygon: true, trash: true },
})
map.addControl(draw)
map.on('draw.create', (e) => {
const feature = e.features[0]
saveFeature(feature)
})After — Element:
<ElementEditorControls
map={mapRef.current as any}
tools={['polygon']}
onFeatureAdd={(feature) => saveFeature(feature)}
/>Three things change:
- Imperative event listeners become declarative callbacks.
- You get snapping, boolean ops, geometry processing, and schemas that Mapbox GL Draw doesn't have.
- Mapbox GL Draw's custom-mode API isn't directly translated — use
@element-editor/uicomponents to build custom layouts instead.
Why not @mapbox/mapbox-gl-draw
Mapbox GL Draw is fine for basic drawing on Mapbox GL JS. Element replaces it when you need any of:
- A React-first API
- Typed property schemas
- A data table for your features
- Snapping, boolean operations, buffer/simplify/smooth
- MapLibre support in the same codebase (useful if you're migrating off Mapbox tiles)
See the full Element vs Mapbox GL Draw comparison.
Known gotchas
- Mapbox access token. Required for the tiles; stored in
mapboxgl.accessToken. Element's editing layer doesn't make any calls to Mapbox's servers. - CSS imports — two files.
mapbox-gl/dist/mapbox-gl.cssfor the map, plus@element-editor/adapter-maplibre/style.cssfor Element's editing layer and UI. map.on('load', ...). Same as MapTiler and MapLibre — waiting for the style to load before passing the instance is the safe pattern.- TypeScript cast.
map={map as any}: themapbox-glandmaplibre-gltype declarations don't match, but Element only calls methods both engines have.