Skip to content
Leaflet.js Markers — Custom Icons, Popups & Clustering

Leaflet.js Markers — Custom Icons, Popups & Clustering

DodaTech Updated Jun 6, 2026 9 min read

Markers are the most common way to highlight points of interest on a map. Leaflet provides customizable markers with popups, tooltips, and clustering for handling hundreds or thousands of locations.

What You’ll Learn

By the end of this tutorial you will add single and multiple markers to a map, create custom icon styles using images, CSS, and SVG, bind interactive popups and tooltips, and cluster large datasets for performance.

Why Markers Matter

Markers turn a static map into an interactive data display. Think of store locators, delivery tracking pins, or real-time asset tracking. Doda Browser uses custom markers for its location-based bookmarks feature, and Durga Antivirus Pro visualizes threat origins with color-coded severity markers on a global 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:::current
  C:::future
  D:::future
  E:::future
  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 should understand how to create a basic Leaflet map from the Leaflet.js tutorial. You need HTML and JavaScript knowledge.

Basic Marker

Adding a marker is the simplest thing you can do in Leaflet:

var marker = L.marker([51.5, -0.09]).addTo(map);

Think of it like placing a pushpin on a physical map. The coordinates tell Leaflet exactly where on the globe to place the pin.

Marker with Popup

A popup is a speech bubble that appears when the user clicks the marker:

L.marker([51.5, -0.09])
    .addTo(map)
    .bindPopup("<b>Hello!</b><br>I am a popup.")
    .openPopup();  // Opens immediately on load
  • bindPopup() attaches the popup content to the marker. The content is raw HTML — you can use bold, links, images, anything.
  • .openPopup() shows it immediately. Without this, the popup appears only on click.

Marker Options

L.marker([lat, lng], {
    draggable: false,        // Set true to let users drag the marker
    title: "Tooltip text",   // Native HTML title attribute
    alt: "Alternative text", // Accessibility
    opacity: 1.0,
    riseOnHover: false,      // Raise z-index on hover
    zIndexOffset: 0
});

Custom Icons

The default blue marker is fine for quick prototypes, but real applications need branded or color-coded icons.

L.icon — Image-Based Icons

var customIcon = L.icon({
    iconUrl: "marker.png",           // Required: the image file
    iconRetinaUrl: "marker@2x.png",  // High-res for Retina displays
    iconSize: [25, 41],              // [width, height] in pixels
    iconAnchor: [12, 41],            // Which pixel of icon aligns with [lat, lng]
    popupAnchor: [1, -34],           // Where popup opens relative to iconAnchor
    shadowUrl: "marker-shadow.png",  // Drop shadow image
    shadowSize: [41, 41],
    shadowAnchor: [12, 41]
});

L.marker([51.5, -0.09], { icon: customIcon }).addTo(map);

The iconAnchor is critical. The bottom center of the pin tip should align with the coordinate point. For the default marker (25×41), the tip is at [12, 41] (center x, bottom y). If your anchor is wrong, the marker floats above or below the actual coordinate.

DivIcon — CSS/HTML-Based Icons

DivIcons don’t use image files. They render styled HTML elements instead. This is perfect for colored badges, numbers, or emoji markers:

var divIcon = L.divIcon({
    className: "custom-div-icon",
    html: "<div style='background: #e91e63; color: white; padding: 4px 8px; " +
          "border-radius: 4px; font-weight: bold;'>NYC</div>",
    iconSize: [50, 30],
    iconAnchor: [25, 15]
});

L.marker([40.7128, -74.006], { icon: divIcon }).addTo(map);

Important: className applies a CSS class to the marker container. Leaflet’s default CSS may interfere. If your styling looks wrong, set className: "" and style the inner html directly.

Custom SVG Marker

For vector-quality markers that scale at any resolution, use inline SVG:

var svgIcon = L.divIcon({
    className: "",
    html: `<svg width="24" height="40" viewBox="0 0 24 40">
        <path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 28 12 28s12-19 12-28C24 5.4 18.6 0 12 0z"
              fill="#e91e63"/>
        <circle cx="12" cy="12" r="5" fill="white"/>
    </svg>`,
    iconSize: [24, 40],
    iconAnchor: [12, 40]
});

Popups in Depth

Popups display information about a location. They support rich HTML:

marker.bindPopup(
    '<div style="min-width:200px;">' +
        '<h3 style="margin: 0 0 5px 0;">Location Name</h3>' +
        '<p style="margin: 0; color: #666;">Description text here.</p>' +
        '<hr style="margin: 8px 0;">' +
        '<p><strong>Lat:</strong> ' + marker.getLatLng().lat.toFixed(4) + '</p>' +
        '<a href="#" onclick="alert(\'Action!\')">View Details</a>' +
    '</div>'
);

Popup Options

marker.bindPopup("Content", {
    maxWidth: 300,         // Maximum popup width
    minWidth: 50,
    maxHeight: null,       // Set a number to enable scroll
    autoPan: true,         // Pan map to keep popup visible
    closeButton: true,     // Show X button
    closeOnClick: true,    // Close when clicking elsewhere
    keepInView: false,     // Keep popup visible when panning
    className: "custom-popup"
});

Tooltips

Tooltips appear on hover, not click. They are subtle and stay out of the way:

marker.bindTooltip("I am a tooltip");

// Sticky tooltip follows the mouse
marker.bindTooltip("Mouse-following tooltip", {
    sticky: true,
    direction: "top"
});

When to use tooltips vs popups: Tooltips for quick labels (city names), popups for detailed information (address, phone, description).

Multiple Markers from Data

Real apps don’t hardcode one marker — they loop through data:

var locations = [
    { name: "London", lat: 51.5074, lng: -0.1278 },
    { name: "Paris", lat: 48.8566, lng: 2.3522 },
    { name: "Berlin", lat: 52.5200, lng: 13.4050 }
];

locations.forEach(function(loc) {
    L.marker([loc.lat, loc.lng])
        .addTo(map)
        .bindPopup("<b>" + loc.name + "</b>");
});

Each marker gets its own popup with the city name. The map automatically shows all three.

Marker Clustering

With 100+ markers, the browser slows down. Clustering groups nearby markers into a single numbered icon that expands on zoom.

Setup

<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css" />
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>

Usage

var markers = L.markerClusterGroup({
    chunkedLoading: true,        // Process markers in batches
    maxClusterRadius: 80,        // Merge markers within 80px
    spiderfyOnMaxZoom: true,     // Spread markers at max zoom
    disableClusteringAtZoom: 16  // Show individual markers at zoom 16+
});

// Add 1000 random markers
for (var i = 0; i < 1000; i++) {
    var lat = Math.random() * 180 - 90;
    var lng = Math.random() * 360 - 180;
    markers.addLayer(L.marker([lat, lng]));
}

map.addLayer(markers);

The cluster shows a number like “15” instead of 15 overlapping pins. As you zoom in, clusters break apart until you see individual markers.

Draggable Markers

Allow users to reposition a marker, like setting a delivery address:

var marker = L.marker([51.5, -0.09], { draggable: true }).addTo(map);

marker.on("dragend", function(e) {
    var pos = e.target.getLatLng();
    marker.bindPopup("New position: " + pos.lat.toFixed(4) + ", " + pos.lng.toFixed(4)).openPopup();
});

Common Mistakes

1. Marker not visible after creation

Markers aren’t visible if they’re outside the current map view. Either set the view to include them or call map.fitBounds() after adding all markers.

2. iconAnchor misalignment

If the iconAnchor is wrong, the marker appears to float above or below its actual coordinate. For a 25×41 icon, the anchor should be [12, 41] (center bottom) unless your image is designed differently.

3. DivIcon className overrides your styles

DivIcon applies Leaflet’s default CSS classes to the container. If your custom styles aren’t applying, set className: "" and put all styling in the html property.

4. Popup content not showing (HTML escaped)

Popup content is raw HTML, not text. If your content shows as literal text, you’re probably using .textContent instead of .innerHTML somewhere.

5. Too many markers causing lag

Without clustering, ~1000 markers will slow the browser. Use L.markerClusterGroup for 100+ markers and consider canvas rendering for 5000+.

Practice Questions

Q1: What does iconAnchor do in L.icon()? A: It defines which pixel of the icon image sits exactly on the coordinate point. For a pushpin, this is typically the bottom center.

Q2: What is the difference between a popup and a tooltip? A: Popups appear on click and contain rich HTML. Tooltips appear on hover and are meant for short labels. Popups are interactive; tooltips are passive.

Q3: How do you add the same popup content to 50 markers efficiently? A: Loop through your data array, create each marker inside the loop, and call bindPopup() with the appropriate data for each one.

Q4: Why use L.markerClusterGroup? A: To group nearby markers into clusters at low zoom levels, preventing visual clutter and improving performance with large datasets.

Q5: What happens if you don’t set iconAnchor on a custom icon? A: Leaflet defaults to [0, 0] (top-left), making the marker appear offset from the actual coordinate point.

Challenge: Build a store locator with 20+ simulated stores in your city. Each store should have a numbered DivIcon marker, a popup with the store name and address, and a sidebar list that highlights the marker when clicked.

FAQ

Can I use emoji as a marker icon?
Yes, use a DivIcon: L.divIcon({ html: "📍", className: "", iconSize: [24, 24] }).
How do I change the default blue marker color?
Use L.icon() with a custom image, or use DivIcon with CSS/SVG to create colored markers without external image files.
What is the maximum number of markers Leaflet can handle?
Without clustering, ~1000 before performance degrades. With MarkerCluster, 10,000+ markers are manageable.
How do I add a popup to a marker programmatically?
Call marker.openPopup() after binding the popup with marker.bindPopup("...").
Can markers have different icons based on data?
Yes, use a conditional inside your loop to select the appropriate icon based on properties (type, severity, status).

Try It Yourself

Here’s a complete store locator with numbered markers, popups, and an interactive sidebar:

<!DOCTYPE html>
<html>
<head>
    <title>Store Locator</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: 450px; border-radius: 8px; }
        .store-list { margin-top: 10px; display: flex; gap: 10px; flex-wrap: wrap; }
        .store-card { border: 1px solid #ddd; border-radius: 6px; padding: 10px;
            cursor: pointer; background: #f8f9fa; min-width: 150px; }
        .store-card:hover { background: #e3f2fd; }
        .store-card h4 { margin: 0 0 4px 0; }
        .store-card p { margin: 0; font-size: 12px; color: #666; }
    </style>
</head>
<body>
    <h2>Store Locator</h2>
    <div id="map"></div>
    <div class="store-list" id="storeList"></div>

    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script>
        var stores = [
            { name: "Downtown", lat: 40.7128, lng: -74.006, phone: "212-555-0100" },
            { name: "Midtown", lat: 40.7580, lng: -73.9855, phone: "212-555-0200" },
            { name: "Brooklyn", lat: 40.6782, lng: -73.9442, phone: "718-555-0300" }
        ];

        var map = L.map("map").setView([40.73, -73.94], 12);
        L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
            attribution: "&copy; OpenStreetMap"
        }).addTo(map);

        var markers = [];
        stores.forEach(function(store, i) {
            var icon = L.divIcon({
                className: "",
                html: "<div style='background:#e91e63;color:white;border-radius:50%;" +
                      "width:28px;height:28px;display:flex;align-items:center;" +
                      "justify-content:center;font-weight:bold;box-shadow:0 2px 4px rgba(0,0,0,0.3);'>" +
                      (i + 1) + "</div>",
                iconSize: [28, 28],
                iconAnchor: [14, 14]
            });
            var marker = L.marker([store.lat, store.lng], { icon: icon })
                .addTo(map)
                .bindPopup("<b>" + store.name + "</b><br>" + store.phone);
            markers.push(marker);
        });

        var list = document.getElementById("storeList");
        stores.forEach(function(store, i) {
            var card = document.createElement("div");
            card.className = "store-card";
            card.innerHTML = "<h4>" + store.name + "</h4><p>" + store.phone + "</p>";
            card.onclick = function() {
                markers[i].openPopup();
                map.setView([store.lat, store.lng], 15);
            };
            list.appendChild(card);
        });
    </script>
</body>
</html>

What’s Next

LessonDescription
Shapes & GeoJSON →Circles, polylines, polygons, and importing geospatial data
Layers & Controls →Layer groups, image overlays, and custom UI controls
Events →Click handlers, hover interactions, and event propagation

Related Topics: SVG for vector icons | JavaScript arrays and loops | HTML popup templates

What’s Next

Congratulations on completing this Leafletjs Markers 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