Draw and Edit Shapes in Leaflet with Leaflet.draw
Leaflet.draw adds a toolbar for drawing polygons, rectangles, lines and markers, and for editing and deleting them afterwards. This example measures every shape and keeps a GeoJSON copy of everything drawn in the box on the right, which is the part you would send to your server. It starts with one saved shape to show how earlier drawings come back as editable shapes.
const map = L.map("map").setView([47.2735, 11.3945], 15);
L.tileLayer("https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{r}.png?api_key=YOUR_API_KEY", {
maxZoom: 18,
attribution:
"© <a href='https://www.maptoolkit.com' target='_blank'>Maptoolkit</a> " +
"© <a href='https://www.openstreetmap.org/copyright' target='_blank'>OSM</a>",
}).addTo(map);
// Everything drawn goes into this group, and the edit tools work on it.
const drawnItems = new L.FeatureGroup().addTo(map);
map.addControl(new L.Control.Draw({
edit: { featureGroup: drawnItems },
draw: { circle: false, circlemarker: false },
}));
function measure(layer) {
if (layer instanceof L.Polygon) {
const squareMeters = L.GeometryUtil.geodesicArea(layer.getLatLngs()[0]);
return (squareMeters / 10000).toFixed(2) + " ha";
}
if (layer instanceof L.Polyline) {
const points = layer.getLatLngs();
let meters = 0;
for (let i = 1; i < points.length; i++) meters += points[i - 1].distanceTo(points[i]);
return (meters / 1000).toFixed(2) + " km";
}
return "";
}
function showGeoJSON() {
document.getElementById("geojson").value =
drawnItems.getLayers().length ? JSON.stringify(drawnItems.toGeoJSON()) : "";
}
map.on(L.Draw.Event.CREATED, (event) => {
drawnItems.addLayer(event.layer);
if (!(event.layer instanceof L.Marker)) event.layer.bindPopup(measure(event.layer)).openPopup();
showGeoJSON();
});
map.on(L.Draw.Event.EDITED, (event) => {
event.layers.eachLayer((layer) => layer.getPopup()?.setContent(measure(layer)));
showGeoJSON();
});
map.on(L.Draw.Event.DELETED, showGeoJSON);
// Shapes saved earlier go back in one layer at a time, so the edit tools can reach them.
const saved = {
type: "Feature",
properties: {},
geometry: {
type: "Polygon",
coordinates: [[[11.3905, 47.2755], [11.3985, 47.2755], [11.3985, 47.2715], [11.3905, 47.2715], [11.3905, 47.2755]]],
},
};
L.geoJSON(saved).eachLayer((layer) => {
drawnItems.addLayer(layer);
layer.bindPopup(measure(layer));
});
showGeoJSON();<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<style>
html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
#map { width: 100%; height: 100%; }
#geojson {
position: absolute; top: 10px; right: 10px; z-index: 1000; width: 260px; height: 140px;
font: 11px/1.4 monospace; padding: 6px; border: none; border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
</style>
</head>
<body>
<div id="map"></div>
<textarea id="geojson" readonly placeholder="Draw a shape with the toolbar on the left."></textarea>
<script>
const map = L.map("map").setView([47.2735, 11.3945], 15);
L.tileLayer("https://rtc-cdn.maptoolkit.net/rtc/maptoolkit-maptoolkit.summer/{z}/{x}/{y}{r}.png?api_key=YOUR_API_KEY", {
maxZoom: 18,
attribution:
"© <a href='https://www.maptoolkit.com' target='_blank'>Maptoolkit</a> " +
"© <a href='https://www.openstreetmap.org/copyright' target='_blank'>OSM</a>",
}).addTo(map);
// Everything drawn goes into this group, and the edit tools work on it.
const drawnItems = new L.FeatureGroup().addTo(map);
map.addControl(new L.Control.Draw({
edit: { featureGroup: drawnItems },
draw: { circle: false, circlemarker: false },
}));
function measure(layer) {
if (layer instanceof L.Polygon) {
const squareMeters = L.GeometryUtil.geodesicArea(layer.getLatLngs()[0]);
return (squareMeters / 10000).toFixed(2) + " ha";
}
if (layer instanceof L.Polyline) {
const points = layer.getLatLngs();
let meters = 0;
for (let i = 1; i < points.length; i++) meters += points[i - 1].distanceTo(points[i]);
return (meters / 1000).toFixed(2) + " km";
}
return "";
}
function showGeoJSON() {
document.getElementById("geojson").value =
drawnItems.getLayers().length ? JSON.stringify(drawnItems.toGeoJSON()) : "";
}
map.on(L.Draw.Event.CREATED, (event) => {
drawnItems.addLayer(event.layer);
if (!(event.layer instanceof L.Marker)) event.layer.bindPopup(measure(event.layer)).openPopup();
showGeoJSON();
});
map.on(L.Draw.Event.EDITED, (event) => {
event.layers.eachLayer((layer) => layer.getPopup()?.setContent(measure(layer)));
showGeoJSON();
});
map.on(L.Draw.Event.DELETED, showGeoJSON);
// Shapes saved earlier go back in one layer at a time, so the edit tools can reach them.
const saved = {
type: "Feature",
properties: {},
geometry: {
type: "Polygon",
coordinates: [[[11.3905, 47.2755], [11.3985, 47.2755], [11.3985, 47.2715], [11.3905, 47.2715], [11.3905, 47.2755]]],
},
};
L.geoJSON(saved).eachLayer((layer) => {
drawnItems.addLayer(layer);
layer.bindPopup(measure(layer));
});
showGeoJSON();
</script>
</body>
</html>Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.
How it works
All drawn shapes live in one L.FeatureGroup. The edit option points the edit and delete
tools at that group, so only shapes inside it can be changed. Leaflet.draw does not add a new
shape anywhere by itself: it fires L.Draw.Event.CREATED, and the handler decides whether the
shape goes into the group.
The three events CREATED, EDITED and DELETED are where the shapes change, so the example
updates the GeoJSON copy in all three. drawnItems.toGeoJSON() returns one FeatureCollection
with everything in the group, ready to store or send.
Saved shapes have to go in one layer at a time. L.geoJSON() returns a group of its own.
Adding that group to drawnItems puts a group inside a group, which the edit tool cannot handle:
it stops with Cannot read properties of undefined (reading 'enable'). eachLayer() adds the
individual polygons instead, and they can be edited like anything drawn in the session.
L.GeometryUtil.geodesicArea() comes with Leaflet.draw and returns square meters measured on the
Earth’s surface, so the area stays correct away from the equator. For lines, distanceTo() on
each pair of points gives meters. A rectangle is a polygon in Leaflet, so it takes the polygon
branch of measure().
Leaflet.draw’s last release was 1.0.4, in 2018. It works with Leaflet 1.9.4 as shown here, and it is what most existing code and answers use. For a maintained alternative with the same kind of toolbar, Leaflet-Geoman has a free version with regular releases.
Next steps
A drawn polygon is a ready-made question for the Maptoolkit APIs. Its coordinates can go straight into the Elevation API to profile a drawn line, or serve as the area you filter your own data by.
To restore a user’s shapes on their next visit, save the toGeoJSON() output and pass it back
through the same L.geoJSON(...).eachLayer() call the example uses for its saved shape.