Skip to content

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.

Use the Maptoolkit Connector. Create a Leaflet map with Maptoolkit raster tiles centered on Innsbruck at zoom level 14, with the Leaflet.draw toolbar for polygons, rectangles, lines and markers. Show the area or length of each shape in a popup, keep the drawn shapes as GeoJSON in a text box, and load one saved polygon as an editable shape.

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.