Skip to main content

alphaThis is a new frontend component. Help us improve it and give your feedback on Slack.

Draw Plugin

The draw plugin lets users draw and edit points, lines and polygons on the map. They can place vertices by click, tap or keyboard, and snap to existing map layers. Shapes are checked as they're built. Polygons can also be split and merged.

Hooks let you add your own checks at each stage: as vertices are placed, as the shape changes, and when it's finished. Use them to apply your own rules, help users fix or review their shapes, and attach your own data.

It works the same with the MapLibre and OpenLayers map providers. The plugin picks the right one from the mapProvider you pass to InteractiveMap, so there's nothing to configure.

IMPORTANT

Using a bundler (ESM)? This plugin includes adapters for more than one map provider, so your bundler needs to ignore the map engines you haven't installed. See Bundler configuration.

ESM usage

import createDrawPlugin from '@defra/interactive-map/plugins/draw'
const drawPlugin = createDrawPlugin()
const interactiveMap = new InteractiveMap('map', {
plugins: [drawPlugin]
})
interactiveMap.on('map:ready', () => {
interactiveMap.addButton('drawPolygon', {
label: 'Draw polygon',
onClick: () => drawPlugin.newPolygon(crypto.randomUUID())
})
})

UMD usage

Copy the entire plugins/draw/dist/umd/ directory to /your-assets-path/plugins/draw/umd/. The plugin uses dynamic imports to load MapLibre or OpenLayers support on demand, so all files in the directory must be served from the same location. Then add the script tag:

<script defer src="/your-assets-path/plugins/draw/umd/index.js"></script>
const drawPlugin = defra.drawPlugin()
const interactiveMap = new defra.InteractiveMap('map', {
mapProvider: defra.maplibreProvider(),
plugins: [drawPlugin]
})

NOTE

GOV.UK Prototype Kit — skip the copy step. All files are served automatically. Use this path instead:

<script defer src="/plugin-assets/%40defra%2Finteractive-map/plugins/draw/dist/umd/index.js"></script>

Options

Options are passed to the factory function when creating the plugin.


snapLayers

Type:
string[]

Vector tile source-layer names to snap new and edited vertices against. Can be overridden per call — see newPolygon, newLine, newPoint, editFeature, and split below.

The layer names available depend entirely on your basemap style — there's no universal default, so check your style's vector tile source(s) for the source-layer names to use. The example below ('OS/TopographicArea_1/Agricultural Land') is specific to an Ordnance Survey basemap style.

When set (globally or per call), a "Snap" toggle button appears in the top row, letting the user turn snapping on and off during a session.

createDrawPlugin({
snapLayers: ['OS/TopographicArea_1/Agricultural Land', 'OS/TopographicLine/Building Outline']
})

snapMinZoom

Type:
number

The lowest zoom snapping works at. Use it to only snap at the zooms where the snap layers are shown.

Below it, the Snap button is disabled and the user's on/off choice is kept. When the user has snap on and zooms below it during a draw or edit, a hint tells them to zoom in. While the button is disabled, the same text is its tooltip and is read by screen readers with the button.

createDrawPlugin({
snapLayers: ['OS/TopographicLine/Field Boundary'],
snapMinZoom: 16
})

snapZoomInHintText

Type:
string
Default:
'Zoom in to use snap'

Hint shown when the user zooms below snapMinZoom, and read with the Snap button while it's disabled.


onGeometryPreview

Type:
Function

Plugin-level synchronous check, called while a shape is drawn or dragged and when a vertex is about to be placed. Can be overridden per call. See onGeometryPreview.


onGeometryChange

Type:
Function

Plugin-level check, called after each committed change to a shape. Can return a Promise, so the shape can be checked against a service. Can be overridden per call. See onGeometryChange.


onGeometryFinish

Type:
Function

Plugin-level check, called when a shape is finished. Can return a Promise. Can be overridden per call. See onGeometryFinish.


Colour and size overrides

NOTE

Each colour accepts a plain colour string or a style-keyed object (e.g. { light: '#1d70b8', dark: '#ffffff' }).

PropertyDescription
shapeStrokeType: string | Record<string, string>
Default stroke colour for an inactive (not being drawn/edited) shape, when the feature sets no stroke of its own
shapeFillType: string | Record<string, string>
Default fill colour for an inactive shape, when the feature sets no fill of its own
strokeWidthType: number
Default: 2
Default stroke width in pixels
editStrokeType: string | Record<string, string>
Stroke colour of the shape currently being drawn or edited
editFillType: string | Record<string, string>
Fill colour of the shape currently being drawn or edited
editVertexType: string | Record<string, string>
Colour of placed, unselected vertex handles
editMidpointType: string | Record<string, string>
Colour of the midpoint handles used to insert a new vertex on an edge
editActiveType: string | Record<string, string>
Colour of the currently selected/active vertex handle
editHaloType: string | Record<string, string>
Colour of the halo drawn behind vertex/midpoint handles for contrast
invalidStrokeType: string | Record<string, string>
Stroke colour of the dashed outline shown while the shape fails validation
splitValidType: string | Record<string, string>
Colour of the split line while it would produce a valid split
splitInvalidType: string | Record<string, string>
Colour of the split line while it would not produce a valid split
snapVertexType: string | Record<string, string>
Colour of the snap indicator shown over a vertex
snapEdgeType: string | Record<string, string>
Colour of the snap indicator shown over an edge
snapRadiusType: number
Default: 12
Snap tolerance in pixels
createDrawPlugin({
shapeStroke: { light: '#1d70b8', dark: '#5694ca' },
editStroke: '#0b0c0c',
strokeWidth: 3,
snapRadius: 16
})

Validation

Every change is checked against the built-in rules, then against your own callbacks if you set them.

Built-in rules

RuleDescription
Minimum verticesA polygon needs 3 vertices and a line 2 before it can be finished
Self-intersectionA polygon that crosses itself can't be finished. A vertex that would make the drawn path cross itself is rejected and never placed
Non-zero areaA flat polygon, with all its vertices in a line, can't be finished

While a rule fails, the shape has a dashed outline.

Callbacks

CallbackDescription
onGeometryPreviewLive checks while a shape is drawn or dragged, and before a vertex is placed. Must answer straight away
onGeometryChangeAfter each change. Can return a Promise. The latest result enables or disables Done
onGeometryFinishWhen a shape is finished. Can return a Promise. Decides whether the shape is created, or the edit finishes

Setting the callbacks

Set them at plugin level, createDrawPlugin({ onGeometryPreview, onGeometryChange, onGeometryFinish }), or per call in the options of newPolygon, newLine, newPoint and editFeature. Each per-call callback replaces only its plugin-level equivalent for that session. The others still apply. To do more than one thing on a change, call each from one callback.

They aren't used by split, merge, addFeature, setStyle or setProperties.

What they return

ValueDescription
trueValid
undefinedValid
falseInvalid, with no reason shown
result.validWhether the shape is valid
result.reasonWhy the shape is invalid. Shown as a hint and sent with draw:geometryinvalid
result.announcementRead out by screen readers instead of draw's own message. When it's set, reason isn't shown as a hint, so you can show it yourself. From onGeometryPreview, it's only read out for a rejected vertex
result.propertiesProperties to add to the finished shape. onGeometryFinish only, on a passing result. See Adding properties to the finished shape
result.reopentrue to reopen a new shape in edit mode, valid, instead of creating it. onGeometryFinish only, on a passing result for phase 'create'. See Reopening a shape that passes

What they receive

PropertyDescription
event.featureThe shape being checked, as a GeoJSON feature
event.phaseWhy the callback was called. See each callback below
event.modeThe draw mode: 'draw_polygon', 'draw_line', 'draw_point', 'edit_vertex' or 'edit_point'
event.vertexIndexThe vertex being placed or changed. Only on 'place', and on a 'change' to a single vertex
event.numVerticesHow many vertices have been placed, not counting the one following the pointer. Only on 'preview'
event.detailThe detail passed to recheckGeometry. Only on 'recheck'
context.signalAn AbortSignal, aborted when a newer check starts or the session ends. Pass it to fetch to cancel the request. Not passed to onGeometryPreview
context.revisionA number that increases with every check. Not passed to onGeometryPreview

Debouncing

Debouncing and timeouts are up to you. Waiting on signal gives a debounce that cancels itself when the shape changes again:

const wait = (ms, signal) => new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms)
signal.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason) }, { once: true })
})
createDrawPlugin({
onGeometryPreview: (event) => ({
valid: isEastOfWalesBorder(event.feature.geometry),
reason: 'Points must be placed east of the England/Wales border'
}),
onGeometryChange: async ({ feature }, { signal }) => {
infoPanel.update(measureArea(feature))
await wait(300, signal)
const response = await fetch('/api/boundary-checks', { method: 'POST', body: JSON.stringify(feature), signal })
return response.json() // { valid, reason }
}
})

onGeometryPreview(event)

Signature:
onGeometryPreview(event) => result

Must answer straight away. A Promise counts as valid. It runs on every frame of a drag, so keep it quick.

PhaseWhen
'preview'While a shape is drawn or dragged. Drives the dashed outline and the Add point button. While editing, it also enables or disables Done
'place'Before a vertex is placed by a click, tap, Enter or Add point. An invalid result rejects the vertex

Use it for rules about a single vertex, such as where the first point can go. onGeometryChange isn't called until the shape has its minimum number of vertices.

onGeometryChange(event, context)

Signature:
onGeometryChange(event, context) => result | Promise<result>

Called after each change to a shape.

PhaseWhen
'change'The shape changed, including with setGeometry or undo
'edit-start'editFeature opened the shape
'recheck'recheckGeometry was called
'create'Only without onGeometryFinish. A new shape was finished without a passing result, such as a new point
'edit-finish'Only without onGeometryFinish, when the last result set recheckOnFinish. Done was pressed on an edit

It's only called once the built-in rules pass, and only the result for the latest change counts. If it fails, throws or rejects, the shape can't be finished: call recheckGeometry() to retry. If the shape is finished while a check is running, draw waits for the result.

While a check is running, Done stays as the last result of onGeometryChange or onGeometryFinish left it: disabled if that result failed, otherwise enabled. When editFeature opens a shape, Done is disabled until its 'edit-start' check passes.

onGeometryFinish(event, context)

Signature:
onGeometryFinish(event, context) => result | Promise<result>

Called when a shape is finished, to check the whole shape once. Without it, finishing uses the latest onGeometryChange result. If onGeometryChange already checks the shape with your service, you don't need onGeometryFinish.

PhaseWhen
'create'A new shape or point was finished
'edit-finish'Done was pressed on an edit

Draw waits for the result. A new shape that fails, or passes with reopen, reopens in edit mode (draw:reopened), where Undo starts empty and Cancel discards it. An edit that fails stays open. If the latest onGeometryChange result failed, the shape can't be finished, so onGeometryFinish isn't called. If an onGeometryChange check is still running when the shape is finished, it's cancelled and onGeometryFinish decides.

This updates a panel on every change, and validates the shape once it's finished:

createDrawPlugin({
// Returns nothing, so every change counts as valid
onGeometryChange: ({ feature }, { signal }) => {
contextPanel.update(feature, { signal })
},
onGeometryFinish: ({ feature }, { signal }) => validateWithService(feature, { signal })
})

Don't await the panel update. onGeometryChange would then be pending until the panel replies: a slow reply is announced as "Checking shape", and finishing the shape cancels it.

Adding properties to the finished shape

A passing result can include properties to store with the shape, such as what the check found. Draw adds them before the shape is created or the edit finishes, so they're in draw:created and draw:edited. They follow the same rules as setProperties.

createDrawPlugin({
onGeometryFinish: async ({ feature }, { signal }) => {
try {
return await validateWithService(feature, { signal })
} catch {
// Let the user finish, but record that the shape wasn't checked
return { valid: true, properties: { drawRules: { checked: false, reason: 'error' } } }
}
}
})

Keep them under one key of your own, such as drawRules, so they don't clash with other properties. properties is ignored on a failing result, and from onGeometryPreview and onGeometryChange.

Reopening a shape that passes

A passing result for a new shape can include reopen: true, to show the user something before the shape is created, such as suggestions, without blocking it. The shape reopens in edit mode with Done enabled, and draw:reopened is sent with status: 'valid'.

Pressing Done on the unchanged shape creates it from that same result, including its properties, without calling onGeometryFinish again. If the user changes the shape first, onGeometryFinish is called with phase 'edit-finish'.

createDrawPlugin({
onGeometryFinish: async ({ feature, phase }) => phase === 'create' && hasSuggestions(feature)
? { valid: true, reopen: true, announcement: '1 suggestion for your shape. Check the issues panel' }
: { valid: true }
})

reopen is ignored on a failing result, which reopens the shape anyway, on 'edit-finish', and from onGeometryPreview and onGeometryChange.

Methods

Methods are called on the plugin instance. newPolygon, newLine, newPoint, editFeature, addFeature, setStyle, setProperties, deleteFeature, split, merge, setGeometry and recheckGeometry all need mapProvider.draw to be ready — call them after draw:ready.


newPolygon(featureId, options?)

Start drawing a new polygon.

ArgumentDescription
featureIdType: string
Required
ID to assign the finished feature
options.snapLayersType: string[]
Overrides the plugin-level snapLayers for this session
options.onGeometryPreviewType: Function
Replaces the plugin-level onGeometryPreview for this session — see Validation
options.onGeometryChangeType: Function
Replaces the plugin-level onGeometryChange for this session — see Validation
options.onGeometryFinishType: Function
Replaces the plugin-level onGeometryFinish for this session — see Validation
options.strokeType: string | Record<string, string>
Stroke colour for this shape
options.fillType: string | Record<string, string>
Fill colour for this shape
options.strokeWidthType: number
Stroke width in pixels for this shape
options.propertiesType: Object
Custom GeoJSON properties to set on the finished feature
drawPlugin.newPolygon(crypto.randomUUID(), {
stroke: '#e6c700',
fill: 'rgba(255, 221, 0, 0.1)'
})

newLine(featureId, options?)

Start drawing a new line. Same options as newPolygon.

drawPlugin.newLine(crypto.randomUUID(), {
stroke: { outdoor: '#99704a', dark: '#ffffff' },
strokeWidth: 6
})

newPoint(featureId, options?)

Start drawing a new point. Unlike newPolygon/newLine, it commits as soon as a point is placed — there's no Done step.

ArgumentDescription
featureIdType: string
Required
ID to assign the finished feature
options.snapLayersType: string[]
Overrides the plugin-level snapLayers for this session
options.onGeometryPreviewType: Function
Replaces the plugin-level onGeometryPreview for this session — see Validation
options.onGeometryChangeType: Function
Replaces the plugin-level onGeometryChange for this session — see Validation
options.onGeometryFinishType: Function
Replaces the plugin-level onGeometryFinish for this session — see Validation
options.propertiesType: Object
Custom GeoJSON properties to set on the finished feature
options.symbolType: string
Built-in symbol id — 'pin', 'circle', 'square', 'hexagon', 'triangle', or 'diamond'
options.symbolSvgContentType: string
Custom SVG markup, used instead of symbol
options.symbolBackgroundColorType: string | Record<string, string>
Symbol background colour
options.symbolForegroundColorType: string | Record<string, string>
Symbol foreground colour
options.symbolGraphicType: string
Overrides the symbol's inner graphic (built-in name or SVG path data)
options.symbolHaloWidthType: number
Symbol halo width
options.symbolViewBoxType: string
SVG viewBox, for use with symbolSvgContent
options.symbolAnchorType: [number, number]
Normalised [x, y] anchor point
options.symbolSizeType: 'small' | 'medium' | 'large'
Symbol size — see Symbol Config

These mirror MarkerOptions' symbol-family properties (prefixed with symbol here to sit alongside other feature properties) — see Symbol Config for the full resolution order and SVG token structure. Points with no symbol config render with the plugin's default marker.

drawPlugin.newPoint(crypto.randomUUID(), {
symbol: 'pin',
symbolBackgroundColor: { outdoor: '#d4351c', dark: '#ff6b6b' }
})

editFeature(featureId, options?)

Open an existing feature in edit mode — vertex-edit for a Polygon/LineString, single-coordinate drag for a Point. Returns false (without doing anything) if the feature doesn't exist or the plugin isn't ready yet — check the return value before assuming the edit session started.

ArgumentDescription
featureIdType: string
Required
ID of the feature to edit
options.snapLayersType: string[]
Overrides the plugin-level snapLayers for this session
options.onGeometryPreviewType: Function
Replaces the plugin-level onGeometryPreview for this session — see Validation
options.onGeometryChangeType: Function
Replaces the plugin-level onGeometryChange for this session — see Validation
options.onGeometryFinishType: Function
Replaces the plugin-level onGeometryFinish for this session — see Validation
const editSuccess = drawPlugin.editFeature(selectedFeatureId)
if (!editSuccess) {
return
}

addFeature(feature)

Add a Point, LineString, or Polygon feature directly to the map without a draw session — e.g. loading in existing shapes on draw:ready.

ArgumentDescription
feature.idType: string
Required
Feature ID
feature.typeType: string
Defaults to 'Feature' if omitted
feature.geometryType: GeoJSON.Geometry
Required
Point, LineString, or Polygon geometry
feature.strokeType: string | Record<string, string>
Stroke colour (LineString/Polygon). Replaces any stroke colours in properties
feature.fillType: string | Record<string, string>
Fill colour (Polygon). Replaces any fill colours in properties
feature.strokeWidthType: number
Stroke width in pixels (LineString/Polygon). Replaces any stroke widths in properties
feature.propertiesType: Object
Custom GeoJSON properties — a Point's symbol-family properties go here (see newPoint above)

A feature saved from draw:created or draw:edited keeps its style in properties, so it can be added back as it is.

A feature with a missing/unsupported geometry.type or no geometry.coordinates is logged as a warning and ignored.

interactiveMap.on('draw:ready', () => {
drawPlugin.addFeature({
id: 'test1234',
type: 'Feature',
geometry: { type: 'Polygon', coordinates: [[[-2.879, 54.709], [-2.877, 54.708], [-2.875, 54.708], [-2.879, 54.709]]] },
stroke: 'rgba(0,112,60,1)',
fill: 'rgba(0,112,60,0.2)',
strokeWidth: 2
})
drawPlugin.addFeature({
id: 'monument1',
type: 'Feature',
geometry: { type: 'Point', coordinates: [-2.878, 54.709] },
properties: { symbol: 'square', symbolBackgroundColor: '#28a197' }
})
})

setStyle(featureId, styleChanges)

Update an existing feature's style — stroke/fill/strokeWidth or, for a Point, its symbol-family properties (see newPoint above). Only the keys you pass are changed; anything else the feature already has is left alone. Returns false if the feature doesn't exist.

Setting stroke, fill or strokeWidth replaces all of its values, including colours set for each map style: setStyle(id, { stroke: '#d4351c' }) shows the new colour in every map style. Colours for each style are stored as properties named after the style, such as strokeDark for a style with the ID dark, so avoid giving your own properties names like these.

ArgumentDescription
featureIdType: string
Required
ID of the feature to update
styleChangesType: Object
Required
Any mix of stroke, fill, strokeWidth, or symbol-family properties
drawPlugin.setStyle('monument1', { symbolBackgroundColor: { outdoor: '#1d70b8', dark: '#4c9ed9' } })

setProperties(featureId, properties)

Add or change a feature's own properties, such as details from a form or an ID from your backend. They're kept on the feature, so later draw:edited events include them. Returns false if the feature doesn't exist.

ArgumentDescription
featureIdType: string
Required
ID of the feature to update
propertiesType: Object
Required
Properties to add or change
  • New values replace old ones with the same name. Objects are replaced, not merged.
  • undefined values are skipped. null is kept, to clear a property.
  • Style properties are ignored with a warning: use setStyle. So are draw's internal properties.
  • If the feature is being edited, cancelling the edit keeps the properties.
detailsForm.addEventListener('submit', () => {
drawPlugin.setProperties('field-1', { name: form.name.value, landUse: form.landUse.value })
})

deleteFeature(featureIds)

Remove one or more features from the map.

ArgumentDescription
featureIdsType: string[]
IDs of the features to remove
drawPlugin.deleteFeature(['test1234'])

split(featureId, options?)

Start drawing a line across a polygon to split it in two. Splitting is a pure computation — it does not remove the original feature or add the results itself. Listen for draw:split and call deleteFeature/addFeature yourself.

Always snaps to the polygon's own outline, in addition to any layers in snapLayers.

ArgumentDescription
featureIdType: string
Required
ID of the polygon to split
options.snapLayersType: string[]
Additional layers to snap against, on top of the polygon's own outline
drawPlugin.split(selectedFeatureId)
interactiveMap.on('draw:split', (event) => {
drawPlugin.deleteFeature([event.originalFeatureId])
event.featureCollection.features.forEach((feature, index) => {
drawPlugin.addFeature({
id: `${event.originalFeatureId}-${index === 0 ? 'a' : 'b'}`,
type: feature.type,
geometry: feature.geometry,
properties: feature.properties
})
})
})

merge(featureIds)

Merge multiple contiguous polygons into one. Like split, this is a pure computation — it does not touch the map. Listen for the return value or draw:merge and call deleteFeature/addFeature yourself.

ArgumentDescription
featureIdsType: string[]
IDs of the polygons to merge
Returns:
the merged GeoJSON feature, or null if the merge failed (e.g. the polygons aren't actually contiguous).
drawPlugin.merge(selectedFeatureIds)
interactiveMap.on('draw:merge', (event) => {
drawPlugin.deleteFeature(event.originalFeatureIds)
drawPlugin.addFeature({
id: event.originalFeatureIds[0],
type: event.feature.type,
geometry: event.feature.geometry,
properties: event.feature.properties
})
})

setGeometry(geometry)

Replace the shape in the current edit session, for example with a corrected boundary from a service. The change can be undone, and is checked like any other change, with phase 'change'. Returns false (without doing anything) outside an edit session, or when the geometry type differs from the shape being edited.

ArgumentDescription
geometryType: GeoJSON.Geometry
Required
The new geometry, of the same type as the shape being edited
drawPlugin.setGeometry(correctedBoundary)

recheckGeometry(detail?)

Call onGeometryChange again for the current shape, unchanged, with phase 'recheck'. Use it when your answer may have changed without the shape changing — for example after the user chooses to ignore a warning — or to retry a check that failed. Returns false (without doing anything) outside a draw or edit session, or before the shape has been checked.

ArgumentDescription
detailType: any
Passed to onGeometryChange as event.detail, for example to tell an ignored warning from a retry
const ignoredWarnings = new Set()
let warnings = []
createDrawPlugin({
onGeometryChange: async ({ feature, detail }, { signal }) => {
// Ignoring a warning reuses the last answer; anything else, including a retry, asks again
if (detail?.reason !== 'ignore') {
warnings = await checkBoundary(feature, { signal })
}
return { valid: warnings.every((warning) => ignoredWarnings.has(warning.id)) }
}
})
const onIgnoreWarning = (warning) => {
ignoredWarnings.add(warning.id)
drawPlugin.recheckGeometry({ reason: 'ignore' })
}
const onRetry = () => drawPlugin.recheckGeometry({ reason: 'retry' })

Application mode

While drawing or editing, the plugin sets the 'draw' application mode, which gives the interface over to drawing: every button, panel and control, in every slot, is hidden except draw's own and these defaults:

['mapStyles', 'mapControls', 'scaleBar']

Everything reappears as it was when the draw or edit mode ends. Hidden items stay mounted, so open panels keep their state and scroll position, and modal panels are never hidden. The app root also gets the class im-o-app--mode-draw.

Adjust it with the applicationModes option, keyed by the mode id. For example, to also keep search and a control of your own, and hide the scale bar:

new InteractiveMap('map', {
applicationModes: {
draw: { include: ['search', 'myControl'], exclude: ['scaleBar'] }
}
})

Or set draw: false when every button on the map is deliberate — for example a single-task map that goes straight into editing a shape — so nothing is hidden.

NOTE

The mode only changes the interface. Other plugins' own behaviour keeps running while their buttons are hidden, so disable any that shouldn't respond while drawing.

IMPORTANT

If you use the interact plugin, disable it while drawing or editing. Otherwise map clicks select features as well as placing points, and both plugins show and hide the same crosshair. Call interactPlugin.disable() on draw:started and draw:editstart, and interactPlugin.enable() on draw:created, draw:edited and draw:cancelled.

Buttons and keyboard shortcuts

The plugin registers its own toolbar buttons automatically — Cancel, Add point and Done in the actions bar, plus Undo, Snap and Delete point in the middle of the top row — which show and enable themselves based on the current draw/edit state. You don't need to render these yourself; augment them with your own trigger buttons (e.g. "Draw polygon", "Draw line") the way the Draw tools example does.

NOTE

While drawing, a crosshair marks where the next point is placed. On touch, it's shown with the Add point button. Keyboard users get the crosshair and press Enter to add a point. Opening the map controls shows the crosshair and Add point button for every input type. Edit modes don't use the crosshair: vertices and midpoints are selected directly.

ShortcutAction
EnterAdd point (draw)
SpacebarSelect nearest point (edit)
Option/Alt + ↑/↓/←/→Select adjacent point (edit)
↑/↓/←/→Move point (edit)
Shift + ↑/↓/←/→Nudge point, fine step (edit)
DeleteDelete point (edit)
Command/Ctrl + ZUndo

A selected edit vertex also claims the map's enableMapControls D-pad, if enabled, so it can be nudged with the on-screen directional buttons as well as the keyboard.

Events

Subscribe to events using interactiveMap.on().


draw:ready

Emitted once the draw plugin has initialised. Safe to call API methods from here.

Payload:
None

draw:started

Emitted when newPolygon, newLine, or newPoint starts a new draw session.

Payload:
PropertyDescription
payload.modeType: 'draw_polygon' | 'draw_line' | 'draw_point'
The draw mode
payload.featureIdType: string
ID of the new feature

draw:editstart

Emitted when editFeature opens an existing feature for editing.

Payload:
PropertyDescription
payload.modeType: 'edit_polygon' | 'edit_line' | 'edit_point'
The kind of shape being edited
payload.featureIdType: string
ID of the feature being edited

draw:created

Emitted when a new shape finishes drawing and passes its checks. Also fires for a new shape that was reopened in edit mode (draw:reopened) and then finished — from the caller's perspective it's still a creation, not an edit.

Payload:
the finished GeoJSON.Feature

draw:edited

Emitted when an existing feature finishes an edit session (via editFeature).

Payload:
the edited GeoJSON.Feature

draw:reopened

Emitted when a new shape is finished and reopens in edit mode: because its check failed, or because a passing onGeometryFinish result asked to reopen it (see Reopening a shape that passes). See Validation.

Payload:
PropertyDescription
payload.featureType: GeoJSON.Feature
The reopened shape
payload.statusType: 'valid' | 'invalid' | 'error'
'valid' if a passing result asked to reopen it, 'invalid' if the check failed, 'error' if it threw or rejected
payload.reasonType: string | null
Why the check failed, or null if it passed or the result gave no reason

draw:cancelled

Emitted when the Cancel button is pressed during a draw or edit session. Cancelling a new shape that's waiting on its check, or has reopened in edit mode, removes it from the map. Also emitted when a new session starts while a finished shape is still waiting on its check, which discards it.

Payload:
the feature being drawn/edited at the time of cancellation

draw:updated

Emitted after a committed vertex operation (add/move/insert/delete) while editing. It isn't emitted while a new shape is being drawn. To keep a panel up to date while drawing and editing, use onGeometryChange.

Payload:
the updated GeoJSON.Feature

draw:vertexselection

Emitted when the selected vertex changes in edit mode.

Payload:
PropertyDescription
payload.indexType: number
The selected vertex, or -1 when nothing is selected
payload.numVerticesType: number
How many vertices the shape has

draw:interfacetypechange

Emitted when the input device changes mid-session (e.g. switching from mouse to touch and panning via the Move control) — sync your own interfaceType state from this if you're tracking it independently.

Payload:
PropertyDescription
payload.interfaceTypeType: 'mouse' | 'touch' | 'keyboard'
The input device now in use

draw:geometryinvalid

Emitted whenever a check — a built-in rule, a placement rejected by onGeometryPreview, or an invalid result from onGeometryChange for the latest change — fails. Not emitted for the 'edit-start' check of a feature that was already invalid. The reason is also shown as a hint toast, unless the result has its own announcement, or the shape simply hasn't reached its minimum vertex count yet. See Validation.

Payload:
PropertyDescription
payload.featureType: GeoJSON.Feature
The shape that failed the check
payload.reasonType: string
Why the check failed
payload.sourceType: 'rules' | 'callback'
'rules' for a built-in rule, 'callback' for onGeometryPreview, onGeometryChange or onGeometryFinish
payload.phaseType: string
The phase of the check that failed. See Validation
payload.modeType: string
The draw mode
payload.vertexIndexType: number
The vertex involved, when there is one

draw:add

Emitted after addFeature adds a feature to the map.

Payload:
the added GeoJSON.Feature

draw:delete

Emitted after deleteFeature removes a feature.

Payload:
PropertyDescription
payload.featureIdType: string[]
The IDs passed to deleteFeature

draw:stylechange

Emitted after setStyle updates a feature's style.

Payload:
PropertyDescription
payload.featureIdType: string
ID of the updated feature
payload.propertiesType: Object
The style properties that changed. A removed colour is null, so you can merge this into your saved copy of the feature

draw:propertieschange

Emitted after setProperties updates a feature's properties.

Payload:
PropertyDescription
payload.featureIdType: string
ID of the updated feature
payload.propertiesType: Object
The properties that were changed

draw:split

Emitted when split computes a successful split.

Payload:
PropertyDescription
payload.originalFeatureIdType: string
ID of the polygon that was split
payload.featureCollectionType: GeoJSON.FeatureCollection
The two resulting polygons

draw:merge

Emitted when merge computes a successful merge.

Payload:
PropertyDescription
payload.originalFeatureIdsType: string[]
IDs of the polygons that were merged
payload.featureType: GeoJSON.Feature
The merged polygon
interactiveMap.on('draw:created', (feature) => {
console.log('New feature drawn:', feature.id)
})
interactiveMap.on('draw:geometryinvalid', ({ reason }) => {
console.log('Validation failed:', reason)
})