Leaflet.js Markers — Custom Icons, Popups & Clustering
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
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
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: "© 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
| Lesson | Description |
|---|---|
| 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