Skip to content

Show the User's Location in Leaflet

Leaflet has geolocation built in: map.locate() asks the browser for the position and fires an event with the result. This example puts it behind a Show my location button, draws a blue dot with a circle for the accuracy, and explains what went wrong when the position is not available.

const map = L.map("map").setView([47.2692, 11.4041], 13);

    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);

    // A Leaflet control: onAdd returns the element Leaflet places in the corner.
    const LocateControl = L.Control.extend({
      options: { position: "topright" },
      onAdd() {
        const container = L.DomUtil.create("div", "leaflet-bar");
        container.style.border = "none";
        const button = L.DomUtil.create("button", "locate-button", container);
        button.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm8.94 3A9 9 0 0 0 13 3.06V1h-2v2.06A9 9 0 0 0 3.06 11H1v2h2.06A9 9 0 0 0 11 20.94V23h2v-2.06A9 9 0 0 0 20.94 13H23v-2h-2.06zM12 19a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/></svg>Show my location';
        this.message = L.DomUtil.create("div", "locate-message", container);
        this.message.hidden = true;
        L.DomEvent.disableClickPropagation(container);
        L.DomEvent.on(button, "click", () => {
          button.classList.add("busy");
          this.message.hidden = true;
          map.locate({ setView: true, maxZoom: 16, enableHighAccuracy: true });
        });
        this.button = button;
        return container;
      },
    });
    const locateControl = new LocateControl().addTo(map);

    // The blue dot and the accuracy circle, created once and moved on every new fix.
    const dot = L.marker([0, 0], { icon: L.divIcon({ className: "", html: '<div class="you"></div>', iconSize: [20, 20] }) });
    const accuracy = L.circle([0, 0], { radius: 0, color: "#1c7ed6", weight: 1, fillOpacity: 0.12 });

    map.on("locationfound", (event) => {
      locateControl.button.classList.remove("busy");
      dot.setLatLng(event.latlng).addTo(map).bindPopup(`Within ${Math.round(event.accuracy)} m of here`);
      accuracy.setLatLng(event.latlng).setRadius(event.accuracy).addTo(map);
    });

    map.on("locationerror", (event) => {
      locateControl.button.classList.remove("busy");
      locateControl.message.textContent = event.code === 1
        ? "Location access was denied. Allow it in your browser's site settings and try again."
        : `Your location is not available right now (${event.message}).`;
      locateControl.message.hidden = false;
    });
<!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" />
  <script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
    #map { width: 100%; height: 100%; }
    .locate-button {
      display: flex; align-items: center; gap: 6px; padding: 7px 12px; border: none; border-radius: 4px;
      background: #fff; cursor: pointer; font: 600 13px system-ui, sans-serif; color: #303f7e;
    }
    .locate-button svg { width: 16px; height: 16px; fill: currentColor; }
    .locate-button.busy { opacity: 0.6; cursor: progress; }
    .locate-message {
      max-width: 220px; margin-top: 6px; padding: 8px 10px; border-radius: 4px; background: #fff;
      font: 12px/1.4 system-ui, sans-serif; color: #c92a2a;
    }
    .you { width: 14px; height: 14px; border-radius: 50%; background: #1c7ed6; border: 3px solid #fff; box-shadow: 0 0 0 2px rgba(28, 126, 214, 0.35); }
  </style>
</head>
<body>
  <div id="map"></div>
  <script>
    const map = L.map("map").setView([47.2692, 11.4041], 13);

    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);

    // A Leaflet control: onAdd returns the element Leaflet places in the corner.
    const LocateControl = L.Control.extend({
      options: { position: "topright" },
      onAdd() {
        const container = L.DomUtil.create("div", "leaflet-bar");
        container.style.border = "none";
        const button = L.DomUtil.create("button", "locate-button", container);
        button.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm8.94 3A9 9 0 0 0 13 3.06V1h-2v2.06A9 9 0 0 0 3.06 11H1v2h2.06A9 9 0 0 0 11 20.94V23h2v-2.06A9 9 0 0 0 20.94 13H23v-2h-2.06zM12 19a7 7 0 1 1 0-14 7 7 0 0 1 0 14z"/></svg>Show my location';
        this.message = L.DomUtil.create("div", "locate-message", container);
        this.message.hidden = true;
        L.DomEvent.disableClickPropagation(container);
        L.DomEvent.on(button, "click", () => {
          button.classList.add("busy");
          this.message.hidden = true;
          map.locate({ setView: true, maxZoom: 16, enableHighAccuracy: true });
        });
        this.button = button;
        return container;
      },
    });
    const locateControl = new LocateControl().addTo(map);

    // The blue dot and the accuracy circle, created once and moved on every new fix.
    const dot = L.marker([0, 0], { icon: L.divIcon({ className: "", html: '<div class="you"></div>', iconSize: [20, 20] }) });
    const accuracy = L.circle([0, 0], { radius: 0, color: "#1c7ed6", weight: 1, fillOpacity: 0.12 });

    map.on("locationfound", (event) => {
      locateControl.button.classList.remove("busy");
      dot.setLatLng(event.latlng).addTo(map).bindPopup(`Within ${Math.round(event.accuracy)} m of here`);
      accuracy.setLatLng(event.latlng).setRadius(event.accuracy).addTo(map);
    });

    map.on("locationerror", (event) => {
      locateControl.button.classList.remove("busy");
      locateControl.message.textContent = event.code === 1
        ? "Location access was denied. Allow it in your browser's site settings and try again."
        : `Your location is not available right now (${event.message}).`;
      locateControl.message.hidden = false;
    });
  </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 with a Show my location button as a Leaflet control. On click, locate the user, zoom to the position, draw a dot with a circle for the accuracy, and show a message if location access is denied.

How it works

map.locate() wraps the browser’s Geolocation API. With setView: true Leaflet also moves the map to the result, and maxZoom: 16 stops it from zooming in all the way. enableHighAccuracy asks for GPS on devices that have it, which is slower and uses more battery; leave it out if a city-block position is enough.

Ask on a click, not on page load. The first call shows the browser’s permission prompt. Tied to a button, the user knows why the page is asking; fired on load, the prompt comes before the user has seen the map, and a refusal is hard to take back later.

locationfound carries latlng and accuracy, the radius in meters within which the true position lies. The circle is drawn with exactly that radius, so a wide circle means a rough fix. The dot and circle are created once and moved on every new result.

locationerror fires when the position is not available. code 1 means the user or the browser denied access; other codes mean the device could not get a fix or ran out of time. A denial is remembered per site, so the message tells the user where to change it.

Geolocation needs HTTPS. Browsers only offer it on secure pages, and localhost counts as one during development. On plain HTTP the browser refuses the request.

The button is a Leaflet control: L.Control.extend with an onAdd that returns the element, so Leaflet places it in the corner and keeps it clear of the other controls. L.DomEvent.disableClickPropagation stops a click on the button from also reaching the map.

Next steps

To follow the user as they move, pass watch: true to map.locate(); locationfound then fires on every new position until you call map.stopLocate(). For a ready-made control with that and a follow mode, the Leaflet.Locate plugin covers it.

A position is the natural start for the Maptoolkit APIs: pass it to the Routing API as the start point, or to the Geocoding API to show the address the user is at.