@element-editor/adapter-leaflet

Complete API reference for the Leaflet editing layer.


Install

npm install @element-editor/adapter-leaflet leaflet react-leaflet

Peer dependencies

PackageVersion
leaflet^1.9
react-leaflet^5
react^19.2
react-dom^19.2

React 18 is not supported. @element-editor/core and @element-editor/ui are installed as regular dependencies (not bundled). To import from them directly, add them to your own package.json at the same version. The geometry worker is inlined, so no bundler configuration is needed.

CSS import

Warning: Always import the adapter stylesheet. The JavaScript entry doesn't import any CSS; without it, map tiles, markers and the editor UI break silently.

import '@element-editor/adapter-leaflet/style.css'

It includes Leaflet's CSS (leaflet/dist/leaflet.css) and the data table's CSS. It doesn't reset your page: Tailwind's base styles apply only inside [data-elm-theme] elements and the editor's own portals.


Requirements

  • ElementEditorUIProvider must be an ancestor of ElementEditorControls. It provides the error boundary, config, shortcut configuration, events and toasts, and the map controller. ElementEditorControls throws without the shortcut and event contexts.
  • GeoStoreProvider must wrap MapContainer (or any ancestor thereof). It creates the isolated Zustand store. One per editor.
  • The map and all Element Editor UI must be inside an element with data-elm-theme="light" or "dark". It scopes the editor's styles and its keyboard shortcuts.
  • ElementEditorControls must be rendered as a child of MapContainer. It uses useMap() from react-leaflet internally.
import { MapContainer, TileLayer } from 'react-leaflet'
import { ElementEditorUIProvider, GeoStoreProvider, ElementEditorControls } from '@element-editor/adapter-leaflet'
import '@element-editor/adapter-leaflet/style.css'

export default function MyMap() {
  return (
    <ElementEditorUIProvider>
      <GeoStoreProvider>
        <div data-elm-theme="light" style={{ width: '100%', height: '600px' }}>
          <MapContainer center={[48.86, 2.35]} zoom={5} style={{ width: '100%', height: '100%' }}>
            <TileLayer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png" />
            <ElementEditorControls onChange={(data) => console.log(data)} />
          </MapContainer>
        </div>
      </GeoStoreProvider>
    </ElementEditorUIProvider>
  )
}

<ElementEditorControls>

import { ElementEditorControls } from '@element-editor/adapter-leaflet'

Renders no visible HTML. Mounts the Element editing layer into the Leaflet map. Also mounts the shared keyboard-shortcut listeners — do not call useKeyboardShortcuts() again in your tree or every shortcut will fire twice.


ElementEditorControlsProps

PropTypeDefaultRequiredDescription
initialDataFeatureCollectionInput | GeoJSONFeatureInput | null—NoThe starting data: any GeoJSON FeatureCollection (@types/geojson's type-checks) or a single Feature. Applied on mount, with no undo step and no onChange. Data that arrives later (null → data) is applied once too, while the editor is empty and unedited (the controls don't move the map: fit it with ref.fitBounds). Other later changes are ignored, with a development warning: use ref.setFeatureCollection(data, { silent: true }). Invalid data reports an error event and the editor starts empty; features with geometry: null are left out and reported.
schemaSchema—NoA schema added to the store on mount. Re-renders that keep the same schema.id are no-ops.
onChange(data: FeatureCollection) => void—NoFires on every committed change with the full current FeatureCollection.
onSubmit(data: FeatureCollection) => void—NoCalled by ref.submit() when all features pass validation. Receives the collection with _validation stripped. The adapter renders no submit button.
onValidationError(errors: Record<string, string[]>) => void—NoCalled by ref.submit() when features have validation errors. Keys are feature IDs. Blocks onSubmit.
onEvent(event: ElementEditorEvent) => void—NoReceives every message the editor would show the user; when set, no toasts are shown. Wraps a local EventProvider, so you don't need ElementEditorUIProvider onEvent too. Read once at mount. See Events.
onError(error: ElementEditorError) => void | false | string—NoCalled first for every runtime failure, with a stable code, source and English message; return false to stop the toast, or a string to replace its text. Runs before the enclosing ElementEditorUIProvider's handlers. See Events.
layout'panel' | 'minimal' | 'split''panel'NoLayout hint read by the @element-editor/ui panels.
toolsDrawingTool[]all toolsNoRestrict which drawing tools are available. Keyboard shortcuts for excluded tools are suppressed.
maxFeaturesnumber—NoMaximum number of features. Enforced by the store for every way of adding features: drawing tools and their shortcuts are disabled at the limit, duplicate, paste and break apart are refused, and imports are truncated (ImportResult.truncated). Each refusal reports an error event. Undo and redo are never blocked.
readonlybooleanfalseNoDisable all editing interactions. Features remain visible.
onFeatureAdd(feature: GeoJSONFeature) => void—NoFires when a feature is added. Fires after onChange for the same operation.
onFeatureUpdate(id: string, feature: GeoJSONFeature) => void—NoFires when a feature's geometry or properties change. Fires after onChange.
onFeatureDelete(id: string) => void—NoFires when a feature is deleted. Fires after onChange.
onSelectionChange(ids: string[]) => void—NoFires when selection changes. Fires independently of feature data changes.
onFeatureSelect(feature: GeoJSONFeature | null) => void—NoFires with the single selected feature, or null when nothing or several features are selected.
onZoomChange(zoom: number) => void—NoFires when the map zoom changes.
onToolChange(tool: DrawingTool | null) => void—NoFires with the tool that turned on (as ref.getTool() reports it), or null when it turned off.
routingApiKeystring—NoDeprecated (dev warning): set config.network.routing.apiKey on the surrounding ElementEditorUIProvider, which wins when both are set. All servers the controls use (routing, Overpass, search) come from config.network: see Third-party services and self-hosting.
bounds[number, number, number, number] | null—NoLeaflet max bounds: the user can't pan outside this box. A GeoJSON bbox [west, south, east, north] ([lng, lat] order, like your data). null clears. Deprecated: the nested [[south, west], [north, east]] Leaflet-order form still works and logs a one-time dev warning with the bbox equivalent.
getFeatureStyle(feature: GeoJSONFeature) => (FeatureStyle & PathOptions) | undefined | null—NoFeatureStyle plus any Leaflet PathOptions per feature (null/undefined = default look), applied to polygons, lines and point pins (pins take fillColor, then color). fillColor falls back to color, as on MapLibre, so a FeatureStyle callback works unchanged. Wins over config.featureColor per key; the selected look wins over it, hover keeps the colour. Called on every render; keep it fast. See Colouring features.
renderer'canvas' | 'svg''canvas'NoHow unselected polygons and lines are drawn. 'canvas' paints them on one shared <canvas> (in a pane named elmIdleFeatures, just below overlayPane), so panning and zooming stay smooth with thousands of features. 'svg' gives each feature its own <path>; use it if your app styles or queries feature paths through CSS or the DOM. Selected features, editing handles and drawing previews are always SVG. Points are markers in both modes.
onReady(map: L.Map) => void—NoCalled once per map, as soon as Element's layers and panes are on it.

ElementEditorControlsRef

The imperative handle exposed via ref: ElementEditorImperativeAPI<L.Map> from @element-editor/ui, the same surface as ElementEditorRef in @element-editor/react. Data-changing methods behave like the user's own edits (validation, maxFeatures, undo, onChange / onFeature* callbacks) and also work in readonly mode.

interface ElementEditorControlsRef {
  /** Returns the current FeatureCollection. `_validation` is stripped. */
  getFeatureCollection: () => FeatureCollection
  /** One feature by id (hidden ones too), or null for an unknown id. */
  getFeature: (id: string) => GeoJSONFeature | null
  /**
   * Replaces all features (like importGeoJSON(data, 'replace')). Fires onChange and is one undo
   * step unless `{ silent: true }` (no data callbacks) / `{ undoable: false }` (clears the history).
   */
  setFeatureCollection: (data: FeatureCollectionInput, options?: { silent?: boolean; undoable?: boolean }) => void
  /** Returns the current FeatureCollection serialised as GeoJSON text. */
  exportGeoJSON: () => string
  /** Triggers a `features.geojson` file download. */
  exportAs: (format: 'geojson') => void
  /** Loads a FeatureCollection. `mode` defaults to 'replace'. */
  importGeoJSON: (raw: FeatureCollectionInput | unknown, mode?: 'replace' | 'merge') => ImportResult
  /** Adds a feature; returns it as stored (with its id), or null (invalid geometry, id taken, maxFeatures). Leaves the selection alone. */
  addFeature: (feature: GeoJSONFeatureInput, options?: { undoable?: boolean }) => GeoJSONFeature | null
  /** Patches one feature (`properties` / `geometry` / `schemaId` / `hidden`). False for an unknown id or a malformed patch. */
  updateFeature: (id: string, patch: FeaturePatch, options?: { undoable?: boolean }) => boolean
  /** Updates many features in one commit and one undo step. See ElementEditorRef. */
  updateFeatures: (updates: { id: string; patch: FeaturePatch }[], options?: { undoable?: boolean }) => void
  /** Removes features in one undo step; returns how many were removed. */
  removeFeatures: (ids: string[], options?: { undoable?: boolean }) => number
  /** Ids of the selected features. */
  getSelection: () => string[]
  /** Replaces the selection (unknown / hidden ids left out); `fit` zooms to it. Fires onSelectionChange. */
  select: (ids: string[], options?: { fit?: boolean }) => void
  /** Turns a drawing tool on (`null`: off). False when unavailable (unknown, `tools`, maxFeatures, readonly). */
  setTool: (tool: DrawingTool | null) => boolean
  /** The tool that is on, or null (also while a shape operation such as cut or buffer runs). */
  getTool: () => DrawingTool | null
  /** Whether undo() / redo() would change something. */
  canUndo: () => boolean
  canRedo: () => boolean
  /** Validates every feature against its schema and geometry; changes nothing, calls no callback. */
  validate: () => { valid: boolean; errors: Record<string, string[]> }
  /** The L.Map you passed in (null only before the controls have mounted). */
  getMap: () => L.Map | null
  /** Flies the map to `center` (`[lng, lat]`). `zoom` defaults to 15. The `([lat, lng], zoom?)` tuple form is deprecated (dev warning). */
  flyTo: (options: { center: [number, number]; zoom?: number }) => void
  /** Sets the zoom, keeping the center. Returns false if the map isn't ready or the zoom is invalid. */
  setZoom: (zoom: number) => boolean
  /**
   * Fits the map to a bbox tuple `[minLng, minLat, maxLng, maxLat]` or a list
   * of `[lng, lat]` points. No-op when the map controller isn't ready yet.
   */
  fitBounds: (target: [number, number][] | [number, number, number, number]) => void
  /** Validates all features, then calls onSubmit or onValidationError. Returns true when valid. */
  submit: () => boolean
  /** Undo the last committed change. */
  undo: () => void
  /** Redo the last undone change. */
  redo: () => void
  /** Clears the current selection. */
  clearSelection: () => void
}

interface ImportResult {
  skipped: number
  duplicates: number
  truncated: boolean
}

Same methods as ElementEditorRef in @element-editor/react (both are ElementEditorImperativeAPI from @element-editor/ui; here getMap() is typed L.Map). See ElementEditorRef for ImportResult, MutationOptions and the details of each method.

Usage:

import { useRef } from 'react'
import { ElementEditorControls, type ElementEditorControlsRef } from '@element-editor/adapter-leaflet'
import { getFeatureCollectionBBox } from '@element-editor/core' // add @element-editor/core to your package.json

const ref = useRef<ElementEditorControlsRef>(null)

<ElementEditorControls ref={ref} />

// Later:
const data = ref.current?.getFeatureCollection()
ref.current?.setFeatureCollection(newData)
ref.current?.undo()
ref.current?.redo()
ref.current?.clearSelection()
ref.current?.select(['a', 'b'], { fit: true })
ref.current?.setTool('polygon')
const added = ref.current?.addFeature({ type: 'Feature', geometry: { type: 'Point', coordinates: [14.42, 50.09] }, properties: {} })
ref.current?.updateFeature(id, { properties: serverProps }, { undoable: false }) // e.g. a server push: not undoable, survives the user's undo

// Zoom-to-fit after an import:
const bbox = getFeatureCollectionBBox(newData)
if (bbox) ref.current?.fitBounds(bbox)

DrawingTool

type DrawingTool =
  | 'polygon' | 'rectangle' | 'circle' | 'line' | 'point' | 'freehand' | 'route'
  | 'picker' | 'measure' | 'search'

GeoStoreProvider

import { GeoStoreProvider } from '@element-editor/adapter-leaflet'

Creates an isolated Zustand store instance. All @element-editor/ui components and ElementEditorControls within the same provider share this store. Re-exported from @element-editor/ui for convenience — both are the same component.


ElementEditorUIProvider

import { ElementEditorUIProvider } from '@element-editor/adapter-leaflet'

The same component as @element-editor/adapter-maplibre's and @element-editor/ui's (it lives in @element-editor/ui; both adapters re-export it).

Bundles the full UI context stack — ElementEditorErrorBoundary, ElementEditorConfigProvider, ShortcutConfigProvider, EventProvider, MapControllerProvider — in the order @element-editor/ui panels expect. Wrap your map component with this when you want toolbars / panels / modals from @element-editor/ui outside the MapContainer. ElementEditorControls needs it too.

PropTypeDefaultDescription
configElementEditorConfig—Theme mode, tools, UI visibility, toolbar layout, units. See ElementEditorConfig. ui.geojsonEditorOpen opens the inspector on mount (it sets codeModalOpen), so it shows when you render GeoJSONModal from that flag as in the quickstart; pass fallback={<GeoJSONDockPlaceholder />} to its <Suspense> so the controls don't jump while its chunk loads.
accentColorstring—Brand accent: any CSS colour. Wins over config.accentColor. <ElementEditorControls> writes it as --elm-accent onto the nearest [data-elm-theme] ancestor of the map container; Element's portals get it too. Updates live. See Theming.
featureColorstring—Default colour of every feature (polygon outline + fill, lines, points). Wins over config.featureColor; getFeatureStyle and the selected look win over it, hover keeps it. Opacity and weight keep their defaults. Updates live. See Colouring features.
onEvent(event: ElementEditorEvent) => void—Receives every user-facing message; when set, no toasts are shown.
onError(error: ElementEditorError) => void | false | string—Called first for every runtime failure (code, source, English message), including a render crash caught by the error boundary (code: 'crash', the original error is cause). Return false to stop the toast, or a string to replace its text. See Events.
toasterbooleantrueRender Element's own sonner <Toaster>. Its toasts carry a per-instance id, so a <Toaster> your app renders, or a second editor, never shows them twice. Pass false to send toasts to your own untagged <Toaster>.
childrenReactNode—Your map, GeoStoreProvider and UI.

It doesn't set data-elm-theme or ThemeContext; render those yourself (see Theming).


Exports

ExportKindDescription
ElementEditorControlsComponentThe editing layer component.
ElementEditorUIProviderComponentContext stack for the editor and the @element-editor/ui components.
GeoStoreProvider, StoreContextComponent / contextStore provider. Also exported by @element-editor/ui.
MapControllerProvider, useMapController, useSetMapControllerComponent / hooksMap controller context (fly-to, fit-bounds). Re-exported from @element-editor/ui.
useMapInteractionMachineHookThe interaction state machine (draw/select/vertex/transform modes).
FloatingToolbar, EditingToolbar, ActiveDrawingToolbar, useKeyboardShortcutsComponents / hookRe-exported from @element-editor/ui. Don't call useKeyboardShortcuts yourself: ElementEditorControls already mounts it.
FeatureStyleProvider, useFeatureStyleComponent / hookPer-feature style context (what getFeatureStyle feeds).
FitBounds, DrawingLayer, FeatureInteractionLayer, DrawingPreviewLayer, SnapController, TransformControls, RoutePreviewLayer, BoundaryLayer, MeasurementTooltipComponentsIndividual map layers, for advanced use. ElementEditorControls renders them for you, except FitBounds (fits the map to the store's features once on mount; mount it inside the MapContainer if you want that). FeatureInteractionLayer draws every feature once (unselected ones on a shared canvas unless renderer="svg").
toLeaflet, toGeoJSON, findSnapPoint, snapToAngleFunctionsCoordinate conversion and snapping utilities.
useDataStoreHookAccess the data store (features, schemas, groups).
useUIStoreHookAccess the UI store (modal state, tool selection, etc.).
useGeoStoreHookAccess the raw store instances (dataStore, uiStore).
ElementEditorErrorClassRe-exported from @element-editor/core. What onError receives (see Events).
ElementEditorEvent, ElementEditorErrorHandler, ErrorCode, ErrorSourceTypesonEvent / onError types.
ElementEditorControlsProps, ElementEditorControlsRef, DrawingTool, FeatureRenderer, ElementEditorUIProviderProps, MapController, FeatureStyle, FeatureStyleFn, MapInteractionMachine, InteractionPhase, ElementEditorConfig, ResolvedConfig, ToolbarPositionTypes

The data types (FeatureCollection, GeoJSONFeature, Schema, …) are not re-exported. Import them from @element-editor/core.