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' }).
| Property | Description |
|---|---|
shapeStroke | Type: string | Record<string, string>Default stroke colour for an inactive (not being drawn/edited) shape, when the feature sets no stroke of its own |
shapeFill | Type: string | Record<string, string>Default fill colour for an inactive shape, when the feature sets no fill of its own |
strokeWidth | Type: numberDefault: 2Default stroke width in pixels |
editStroke | Type: string | Record<string, string>Stroke colour of the shape currently being drawn or edited |
editFill | Type: string | Record<string, string>Fill colour of the shape currently being drawn or edited |
editVertex | Type: string | Record<string, string>Colour of placed, unselected vertex handles |
editMidpoint | Type: string | Record<string, string>Colour of the midpoint handles used to insert a new vertex on an edge |
editActive | Type: string | Record<string, string>Colour of the currently selected/active vertex handle |
editHalo | Type: string | Record<string, string>Colour of the halo drawn behind vertex/midpoint handles for contrast |
invalidStroke | Type: string | Record<string, string>Stroke colour of the dashed outline shown while the shape fails validation |
splitValid | Type: string | Record<string, string>Colour of the split line while it would produce a valid split |
splitInvalid | Type: string | Record<string, string>Colour of the split line while it would not produce a valid split |
snapVertex | Type: string | Record<string, string>Colour of the snap indicator shown over a vertex |
snapEdge | Type: string | Record<string, string>Colour of the snap indicator shown over an edge |
snapRadius | Type: numberDefault: 12Snap 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
| Rule | Description |
|---|---|
| Minimum vertices | A polygon needs 3 vertices and a line 2 before it can be finished |
| Self-intersection | A 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 area | A flat polygon, with all its vertices in a line, can't be finished |
While a rule fails, the shape has a dashed outline.
Callbacks
| Callback | Description |
|---|---|
onGeometryPreview | Live checks while a shape is drawn or dragged, and before a vertex is placed. Must answer straight away |
onGeometryChange | After each change. Can return a Promise. The latest result enables or disables Done |
onGeometryFinish | When 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
| Value | Description |
|---|---|
true | Valid |
undefined | Valid |
false | Invalid, with no reason shown |
result.valid | Whether the shape is valid |
result.reason | Why the shape is invalid. Shown as a hint and sent with draw:geometryinvalid |
result.announcement | Read 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.properties | Properties to add to the finished shape. onGeometryFinish only, on a passing result. See Adding properties to the finished shape |
result.reopen | true 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
| Property | Description |
|---|---|
event.feature | The shape being checked, as a GeoJSON feature |
event.phase | Why the callback was called. See each callback below |
event.mode | The draw mode: 'draw_polygon', 'draw_line', 'draw_point', 'edit_vertex' or 'edit_point' |
event.vertexIndex | The vertex being placed or changed. Only on 'place', and on a 'change' to a single vertex |
event.numVertices | How many vertices have been placed, not counting the one following the pointer. Only on 'preview' |
event.detail | The detail passed to recheckGeometry. Only on 'recheck' |
context.signal | An AbortSignal, aborted when a newer check starts or the session ends. Pass it to fetch to cancel the request. Not passed to onGeometryPreview |
context.revision | A 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.
| Phase | When |
|---|---|
'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.
| Phase | When |
|---|---|
'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.
| Phase | When |
|---|---|
'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.
| Argument | Description |
|---|---|
featureId | Type: stringRequired ID to assign the finished feature |
options.snapLayers | Type: string[]Overrides the plugin-level snapLayers for this session |
options.onGeometryPreview | Type: FunctionReplaces the plugin-level onGeometryPreview for this session — see Validation |
options.onGeometryChange | Type: FunctionReplaces the plugin-level onGeometryChange for this session — see Validation |
options.onGeometryFinish | Type: FunctionReplaces the plugin-level onGeometryFinish for this session — see Validation |
options.stroke | Type: string | Record<string, string>Stroke colour for this shape |
options.fill | Type: string | Record<string, string>Fill colour for this shape |
options.strokeWidth | Type: numberStroke width in pixels for this shape |
options.properties | Type: ObjectCustom 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.
| Argument | Description |
|---|---|
featureId | Type: stringRequired ID to assign the finished feature |
options.snapLayers | Type: string[]Overrides the plugin-level snapLayers for this session |
options.onGeometryPreview | Type: FunctionReplaces the plugin-level onGeometryPreview for this session — see Validation |
options.onGeometryChange | Type: FunctionReplaces the plugin-level onGeometryChange for this session — see Validation |
options.onGeometryFinish | Type: FunctionReplaces the plugin-level onGeometryFinish for this session — see Validation |
options.properties | Type: ObjectCustom GeoJSON properties to set on the finished feature |
options.symbol | Type: stringBuilt-in symbol id — 'pin', 'circle', 'square', 'hexagon', 'triangle', or 'diamond' |
options.symbolSvgContent | Type: stringCustom SVG markup, used instead of symbol |
options.symbolBackgroundColor | Type: string | Record<string, string>Symbol background colour |
options.symbolForegroundColor | Type: string | Record<string, string>Symbol foreground colour |
options.symbolGraphic | Type: stringOverrides the symbol's inner graphic (built-in name or SVG path data) |
options.symbolHaloWidth | Type: numberSymbol halo width |
options.symbolViewBox | Type: stringSVG viewBox, for use with symbolSvgContent |
options.symbolAnchor | Type: [number, number]Normalised [x, y] anchor point |
options.symbolSize | Type: '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.
| Argument | Description |
|---|---|
featureId | Type: stringRequired ID of the feature to edit |
options.snapLayers | Type: string[]Overrides the plugin-level snapLayers for this session |
options.onGeometryPreview | Type: FunctionReplaces the plugin-level onGeometryPreview for this session — see Validation |
options.onGeometryChange | Type: FunctionReplaces the plugin-level onGeometryChange for this session — see Validation |
options.onGeometryFinish | Type: FunctionReplaces 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.
| Argument | Description |
|---|---|
feature.id | Type: stringRequired Feature ID |
feature.type | Type: stringDefaults to 'Feature' if omitted |
feature.geometry | Type: GeoJSON.GeometryRequired Point, LineString, or Polygon geometry |
feature.stroke | Type: string | Record<string, string>Stroke colour ( LineString/Polygon). Replaces any stroke colours in properties |
feature.fill | Type: string | Record<string, string>Fill colour ( Polygon). Replaces any fill colours in properties |
feature.strokeWidth | Type: numberStroke width in pixels ( LineString/Polygon). Replaces any stroke widths in properties |
feature.properties | Type: ObjectCustom 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.
| Argument | Description |
|---|---|
featureId | Type: stringRequired ID of the feature to update |
styleChanges | Type: ObjectRequired 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.
| Argument | Description |
|---|---|
featureId | Type: stringRequired ID of the feature to update |
properties | Type: ObjectRequired Properties to add or change |
- New values replace old ones with the same name. Objects are replaced, not merged.
undefinedvalues are skipped.nullis 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.
| Argument | Description |
|---|---|
featureIds | Type: 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.
| Argument | Description |
|---|---|
featureId | Type: stringRequired ID of the polygon to split |
options.snapLayers | Type: 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.
| Argument | Description |
|---|---|
featureIds | Type: string[]IDs of the polygons to merge |
- Returns:
- the merged GeoJSON feature, or
nullif 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.
| Argument | Description |
|---|---|
geometry | Type: GeoJSON.GeometryRequired 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.
| Argument | Description |
|---|---|
detail | Type: anyPassed 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.
| Shortcut | Action |
|---|---|
| Enter | Add point (draw) |
| Spacebar | Select nearest point (edit) |
| Option/Alt + ↑/↓/←/→ | Select adjacent point (edit) |
| ↑/↓/←/→ | Move point (edit) |
| Shift + ↑/↓/←/→ | Nudge point, fine step (edit) |
| Delete | Delete point (edit) |
| Command/Ctrl + Z | Undo |
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:
| Property | Description |
|---|---|
payload.mode | Type: 'draw_polygon' | 'draw_line' | 'draw_point'The draw mode |
payload.featureId | Type: stringID of the new feature |
draw:editstart
Emitted when editFeature opens an existing feature for editing.
- Payload:
| Property | Description |
|---|---|
payload.mode | Type: 'edit_polygon' | 'edit_line' | 'edit_point'The kind of shape being edited |
payload.featureId | Type: stringID 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:
| Property | Description |
|---|---|
payload.feature | Type: GeoJSON.FeatureThe reopened shape |
payload.status | Type: 'valid' | 'invalid' | 'error''valid' if a passing result asked to reopen it, 'invalid' if the check failed, 'error' if it threw or rejected |
payload.reason | Type: string | nullWhy 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:
| Property | Description |
|---|---|
payload.index | Type: numberThe selected vertex, or -1 when nothing is selected |
payload.numVertices | Type: numberHow 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:
| Property | Description |
|---|---|
payload.interfaceType | Type: '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:
| Property | Description |
|---|---|
payload.feature | Type: GeoJSON.FeatureThe shape that failed the check |
payload.reason | Type: stringWhy the check failed |
payload.source | Type: 'rules' | 'callback''rules' for a built-in rule, 'callback' for onGeometryPreview, onGeometryChange or onGeometryFinish |
payload.phase | Type: stringThe phase of the check that failed. See Validation |
payload.mode | Type: stringThe draw mode |
payload.vertexIndex | Type: numberThe 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:
| Property | Description |
|---|---|
payload.featureId | Type: string[]The IDs passed to deleteFeature |
draw:stylechange
Emitted after setStyle updates a feature's style.
- Payload:
| Property | Description |
|---|---|
payload.featureId | Type: stringID of the updated feature |
payload.properties | Type: ObjectThe 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:
| Property | Description |
|---|---|
payload.featureId | Type: stringID of the updated feature |
payload.properties | Type: ObjectThe properties that were changed |
draw:split
Emitted when split computes a successful split.
- Payload:
| Property | Description |
|---|---|
payload.originalFeatureId | Type: stringID of the polygon that was split |
payload.featureCollection | Type: GeoJSON.FeatureCollectionThe two resulting polygons |
draw:merge
Emitted when merge computes a successful merge.
- Payload:
| Property | Description |
|---|---|
payload.originalFeatureIds | Type: string[]IDs of the polygons that were merged |
payload.feature | Type: GeoJSON.FeatureThe merged polygon |
interactiveMap.on('draw:created', (feature) => { console.log('New feature drawn:', feature.id)})
interactiveMap.on('draw:geometryinvalid', ({ reason }) => { console.log('Validation failed:', reason)})