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-gl

Element 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:

  1. Imperative event listeners become declarative callbacks.
  2. You get snapping, boolean ops, geometry processing, and schemas that Mapbox GL Draw doesn't have.
  3. Mapbox GL Draw's custom-mode API isn't directly translated — use @element-editor/ui components 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.css for the map, plus @element-editor/adapter-maplibre/style.css for 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}: the mapbox-gl and maplibre-gl type declarations don't match, but Element only calls methods both engines have.

Ready to ship?