Skip to content
Leaflet.js Events — Click Handling, Map Interactions & Event Propagation

Leaflet.js Events — Click Handling, Map Interactions & Event Propagation

DodaTech Updated Jun 6, 2026 9 min read

Leaflet provides a rich event system for handling user interactions — clicks, hovers, drags, zooms, and custom events on the map and individual layers.

What You’ll Learn

By the end of this tutorial you will handle map and layer events, use event objects to get coordinates and targets, control event propagation to prevent conflicts, build right-click context menus, and manage event listeners efficiently.

Why Events Matter

Events are what make a map interactive. Without events, maps are just pictures. With events, users can click to add markers, hover to highlight regions, drag to reposition, and right-click for context menus. Doda Browser uses map click events for location-based bookmark creation. Durga Antivirus Pro uses hover events on threat markers to show real-time threat intelligence data without cluttering the map.

    flowchart LR
  A["Leaflet.js Basics"] --> B["Markers & Icons"]
  B --> C["Shapes & GeoJSON"]
  C --> D["Layers & Controls"]
  D --> E["Events 🖱️"]
  E --> F["Advanced & Plugins"]
  
  A:::past
  B:::past
  C:::past
  D:::past
  E:::current
  F:::future
  
  classDef past fill:#4CAF50,stroke:#333,color:#fff
  classDef current fill:#e91e63,stroke:#333,color:#fff
  classDef future fill:#f5f5f5,stroke:#999,color:#666
  
You need the Leaflet.js and a solid understanding of JavaScript callback functions.

The Event Pattern

Every Leaflet event follows the same pattern:

target.on("eventname", function(eventObject) {
    // Your code here
});
  • target — The map, marker, polygon, or layer that fires the event
  • eventname — A string like "click", "mouseover", "dragend"
  • eventObject — An object with properties like latlng, layer, target

Map Events

Map events fire when the user interacts with the map background (not a specific layer):

map.on("click", function(e) {
    console.log("Clicked at:", e.latlng);
    // e.latlng gives you {lat: 51.5, lng: -0.09}
});

map.on("dblclick", function(e) {
    console.log("Double-clicked at:", e.latlng);
});

map.on("contextmenu", function(e) {   // Right-click
    console.log("Right-click at:", e.latlng);
});

map.on("mousemove", function(e) {
    // Fires constantly as mouse moves — throttle this for performance
    console.log("Mouse at:", e.latlng);
});

View Events

These fire when the map moves or zooms:

map.on("moveend", function() {
    console.log("Moved to:", map.getCenter());
});

map.on("zoomend", function() {
    console.log("Zoomed to:", map.getZoom());
});

map.on("zoomstart", function() {
    console.log("Zoom starting...");
});

map.on("resize", function(e) {
    console.log("Map resized to:", e.newSize);
});

Use moveend and zoomend (not move/zoom) for most cases. The “end” variants fire once when the action completes. The non-end variants fire continuously during the action, which can hurt performance.

Geolocation Events

Leaflet can request the user’s browser location:

map.on("locationfound", function(e) {
    console.log("You are at:", e.latlng);
    console.log("Accuracy:", e.accuracy, "meters");

    L.marker(e.latlng).addTo(map)
        .bindPopup("You are here!").openPopup();

    L.circle(e.latlng, { radius: e.accuracy }).addTo(map);
});

map.on("locationerror", function(e) {
    console.log("Location error:", e.message);
    alert("Could not get your location. Check browser permissions.");
});

// Request the location
map.locate({ setView: true, maxZoom: 16 });

Layer Events

Layer events fire when the user interacts with a specific marker, circle, or polygon:

marker.on("click", function(e) {
    console.log("Marker clicked at:", e.latlng);
});

marker.on("mouseover", function(e) {
    // Highlight on hover
    this.setStyle({ color: "red" });
});

marker.on("mouseout", function(e) {
    this.setStyle({ color: "blue" });
});

// Drag events (requires draggable: true)
marker.on("dragstart", function(e) {});
marker.on("drag", function(e) {});
marker.on("dragend", function(e) {
    var pos = e.target.getLatLng();
    console.log("Dragged to:", pos);
});

// Popup events
marker.on("popupopen", function(e) {});
marker.on("popupclose", function(e) {});

Vector Layer Events

circle.on("mouseover", function(e) {
    this.setStyle({ fillOpacity: 0.7 });
});

circle.on("mouseout", function(e) {
    this.setStyle({ fillOpacity: 0.2 });
});

polygon.on("click", function(e) {
    this.bindPopup("Polygon clicked").openPopup();
});

Feature Group Events (Event Bubbling)

This is where L.featureGroup shines — one listener handles clicks on all children:

var group = L.featureGroup([marker1, marker2]);
group.addTo(map);

group.on("click", function(e) {
    // e.layer refers to the individual marker that was clicked
    console.log("Clicked on:", e.layer);
    e.layer.bindPopup("You clicked me").openPopup();
});

Without event bubbling, you’d need to attach a click handler to every marker individually.

The Event Object

Every event handler receives an event object with these properties:

map.on("click", function(e) {
    e.latlng;          // {lat: 51.5, lng: -0.09} — geographic coordinates
    e.layerPoint;      // {x: 200, y: 150} — pixel position on the map pane
    e.containerPoint;  // {x: 200, y: 150} — pixel position on the screen
    e.originalEvent;   // The native browser MouseEvent
    e.type;            // "click"
    e.target;          // The map object that fired the event
});

marker.on("click", function(e) {
    e.latlng;            // Coordinates of the marker
    e.layer;             // The marker itself
    e.propagatedFrom;    // Where the event originated
    e.target;            // The object the listener was attached to
});

Understanding these properties is key to building interactive features:

  • e.latlng — What you need 90% of the time. Gives you coordinates.
  • e.originalEvent — Gives you the native browser event (for preventDefault(), stopPropagation(), etc.)
  • e.target vs e.layer — With feature groups, e.target is the group, e.layer is the specific child that was clicked.

Managing Event Listeners

// Remove one specific listener
map.off("click", handlerFunction);

// Remove all click listeners
map.off("click");

// Remove ALL listeners
map.off();

// Fire an event only once
map.once("click", function(e) {
    alert("This will only fire once!");
});

// Check if event type has listeners
map.hasEventListeners("click");

Important: Always clean up event listeners when removing layers, especially in single-page applications. Detached listeners cause memory leaks.

Right-Click Context Menu

map.on("contextmenu", function(e) {
    e.originalEvent.preventDefault();  // Hide browser context menu

    var menu = document.createElement("div");
    menu.style.cssText = "position:absolute;background:white;border:1px solid #ccc;" +
        "box-shadow:0 2px 8px rgba(0,0,0,0.15);border-radius:4px;z-index:10000;";

    menu.innerHTML = '<div style="padding:8px 16px;cursor:pointer;" ' +
        'onclick="alert(\'Option 1 at ' + e.latlng.lat.toFixed(4) + ',' +
        e.latlng.lng.toFixed(4) + '\')">Add Marker Here</div>' +
        '<div style="padding:8px 16px;cursor:pointer;" ' +
        'onclick="alert(\'Option 2\')">Show Coordinates</div>';

    menu.style.left = e.containerPoint.x + "px";
    menu.style.top = e.containerPoint.y + "px";
    document.getElementById("map").appendChild(menu);

    // Close menu on next click
    document.addEventListener("click", function close() {
        menu.remove();
        document.removeEventListener("click", close);
    });
});

Event Propagation Control

Sometimes you want a marker click to NOT also fire the map’s click handler:

// Option 1: Disable bubbling on the marker
L.marker([51.5, -0.09], {
    bubblingMouseEvents: false   // Marker click won't bubble to map
});

// Option 2: Stop propagation in the handler
marker.on("click", function(e) {
    L.DomEvent.stopPropagation(e.originalEvent);
    // This marker click won't trigger the map's click handler
});

// Option 3: Use the preclick event (fires before click)
map.on("preclick", function(e) {
    console.log("Map will be clicked at:", e.latlng);
    // Useful for closing popups before a new click
});

Common Mistakes

1. Forgetting the event object parameter

Event handlers receive the event as the first argument. Forgetting it means you can’t access e.latlng:

// Wrong — can't access event data
map.on("click", function() {
    console.log(latlng);  // undefined!
});

// Correct
map.on("click", function(e) {
    console.log(e.latlng);
});

2. Not removing event listeners when removing layers

Detached DOM elements with listeners cause memory leaks. Call map.removeLayer(layer) which handles cleanup, or call marker.off() before removing.

3. this context confusion in event handlers

Inside event handlers, this refers to the layer that fired the event, not the map. Use e.target for consistent access:

marker.on("click", function(e) {
    // Both work, but e.target is more explicit
    this.bindPopup("Hi");     // this = marker
    e.target.bindPopup("Hi"); // e.target = marker
});

4. Popup/tooltip events not firing

popupopen and popupclose fire on the layer (marker/polygon), not the popup itself. Listen on the layer:

marker.on("popupopen", function() {
    console.log("Popup opened");
});

5. Click and dblclick both firing

Double-click always fires two click events before dblclick. If you need to distinguish, add a short delay counter or use dblclick alone to avoid double-firing.

Practice Questions

Q1: How do you get the coordinates of a user’s click on the map? A: Use map.on("click", function(e) { console.log(e.latlng); }). The e.latlng property contains {lat, lng}.

Q2: What is the difference between e.target and e.layer? A: e.target is the object where the listener was attached. e.layer is the specific layer interacted with (useful in feature groups where the group = target, child = layer).

Q3: How do you prevent a marker click from also triggering the map’s click handler? A: Set bubblingMouseEvents: false on the marker, or call L.DomEvent.stopPropagation(e.originalEvent) in the marker’s click handler.

Q4: When would you use map.once() instead of map.on()? A: When you need a handler to fire only the first time an event occurs — for example, showing a one-time welcome message or tutorial hint.

Q5: Why should you avoid using mousemove on the map? A: It fires hundreds of times per second as the mouse moves, which can cause performance issues. If you must use it, throttle the handler.

Challenge: Build an interactive map where left-click adds a marker with a popup showing the coordinates, right-click adds a circle with a 1km radius, and hovering over any marker changes its color. Include a button to clear all user-added layers.

FAQ

How do I get coordinates of a map click?
Use e.latlng in the click handler: map.on("click", function(e) { console.log(e.latlng); }).
Can I add custom keyboard shortcuts to Leaflet?
Leaflet has built-in keyboard nav (arrow keys, +/-). For custom shortcuts, listen to document.onkeydown and call map methods.
How do I prevent popups from closing when clicking the map?
Set closeOnClick: false in popup options: marker.bindPopup("...", { closeOnClick: false }).
What is e.originalEvent?
It’s the native browser DOM event (MouseEvent, KeyboardEvent, etc.), useful for calling preventDefault() or stopPropagation().
Can I listen for events on a layer before adding it to the map?
Yes. Leaflet events work on layers before they’re added. Set up all listeners and then add the layer.

Try It Yourself

Here’s a complete click coordinate logger that adds draggable markers and right-click circles:

<!DOCTYPE html>
<html>
<head>
    <title>Coordinate Logger</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <style>
        body { font-family: sans-serif; padding: 20px; }
        #map { height: 400px; border-radius: 8px; }
        .panel { margin-top: 10px; padding: 10px; background: #f8f9fa;
            border-radius: 6px; border: 1px solid #ddd; }
        .coord { display: inline-block; margin: 2px 5px; padding: 4px 8px;
            background: #e3f2fd; border-radius: 3px; cursor: pointer; }
        .coord:hover { background: #bbdefb; }
    </style>
</head>
<body>
    <h2>Click Coordinate Logger</h2>
    <p>Click to add markers. Right-click for circles.</p>
    <div id="map"></div>
    <div class="panel">
        <strong>Clicked Coordinates:</strong>
        <div id="coordList"></div>
        <button onclick="clearMarkers()" style="margin-top:8px;">Clear All</button>
    </div>

    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script>
        var map = L.map("map").setView([20, 0], 2);
        L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
            attribution: "&copy; OpenStreetMap"
        }).addTo(map);

        var markers = L.featureGroup().addTo(map);
        var count = 0;

        map.on("click", function(e) {
            count++;
            var marker = L.marker(e.latlng, { draggable: true })
                .addTo(markers)
                .bindPopup("<b>Point " + count + "</b><br>" +
                    e.latlng.lat.toFixed(4) + ", " + e.latlng.lng.toFixed(4))
                .openPopup();

            var list = document.getElementById("coordList");
            var el = document.createElement("span");
            el.className = "coord";
            el.textContent = count + ": " + e.latlng.lat.toFixed(4) + ", " + e.latlng.lng.toFixed(4);
            el.onclick = function() {
                marker.openPopup();
                map.setView(e.latlng, 10);
            };
            list.appendChild(el);

            marker.on("dragend", function(ev) {
                var pos = ev.target.getLatLng();
                marker.setPopupContent("<b>Point " + count + "</b><br>" +
                    pos.lat.toFixed(4) + ", " + pos.lng.toFixed(4));
                el.textContent = count + ": " + pos.lat.toFixed(4) + ", " + pos.lng.toFixed(4);
            });
        });

        map.on("contextmenu", function(e) {
            L.circle(e.latlng, {
                radius: 50000, color: "#e91e63", fillOpacity: 0.1
            }).addTo(markers);
        });

        function clearMarkers() {
            markers.clearLayers();
            document.getElementById("coordList").innerHTML = "";
            count = 0;
        }
    </script>
</body>
</html>

What’s Next

LessonDescription
Advanced & Plugins →Heatmaps, routing, performance optimization, and custom CRS
API Reference →Complete cheatsheet and quick reference
Shapes & GeoJSON →GeoJSON loading and styling with event-driven choropleths

Related Topics: JavaScript event handling best practices | REST API data fetching on click

What’s Next

Congratulations on completing this Leafletjs Events tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro