Leaflet.js Layers & Controls — Layer Management, Overlays & Custom Controls
Organizing map elements into layers and providing user controls creates professional, interactive mapping applications that let users choose what they see.
What You’ll Learn
By the end of this tutorial you will manage layers with layer groups and feature groups, use the built-in layers control for toggling base maps and overlays, add image and video overlays, customize built-in controls, and build your own custom controls.
Why Layers & Controls Matter
Imagine a map with markers, heat zones, traffic data, and satellite imagery all crammed together — chaos. Layers let you organize information, and controls let users choose what to see. Doda Browser uses layered maps to separate bookmark pins from heatmaps. Durga Antivirus Pro uses layer toggles to let analysts switch between threat heatmaps, attack paths, and normal map views.
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:::current
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
Layer Groups vs Feature Groups
This is a common point of confusion. Let’s make it clear:
Layer group (L.layerGroup) — A simple collection of layers. Add/remove them all at once. No event bubbling.
Feature group (L.featureGroup) — Extends layer group with event handling and styling methods. Clicks on child layers bubble up to the group.
Layer Group — Simple Collection
var cityMarkers = L.layerGroup([
L.marker([51.5, -0.09]),
L.marker([48.85, 2.35]),
L.marker([52.52, 13.40])
]);
cityMarkers.addTo(map);
// Remove all at once
cityMarkers.remove();
// Clear all children
cityMarkers.clearLayers();
// Loop through children
cityMarkers.eachLayer(function(layer) {
console.log(layer.getLatLng());
});Think of a layer group as a folder on your desktop — it holds files, but clicking a file inside doesn’t “notify” the folder.
Feature Group — With Event Bubbling
var circles = L.featureGroup([
L.circle([51.5, -0.09], { radius: 500, color: "red" }),
L.circle([48.85, 2.35], { radius: 500, color: "blue" })
]);
circles.addTo(map);
// Style ALL children at once
circles.setStyle({ color: "green" });
// Fit map to show all children
map.fitBounds(circles.getBounds());
// Listen for clicks on ANY child
circles.on("click", function(e) {
console.log("Clicked on:", e.layer);
e.layer.bindPopup("You clicked me").openPopup();
});When to use which: Use L.featureGroup whenever you need interactivity (click handlers, hover effects). Use L.layerGroup for simple on/off toggling.
Layers Control
The layers control is a built-in UI widget that lets users switch between base maps and toggle overlays:
// Define base maps (mutually exclusive — only one visible at a time)
var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png");
var dark = L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png");
// Define overlays (can be on/off independently)
var markers = L.marker([51.5, -0.09]).bindPopup("London");
var circle = L.circle([48.85, 2.35], { radius: 5000, color: "red" });
var baseMaps = {
"Street Map": osm,
"Dark Mode": dark
};
var overlayMaps = {
"Markers": markers,
"Circles": circle
};
L.control.layers(baseMaps, overlayMaps, {
collapsed: false, // Always expanded (default: collapsed on small screens)
position: "topright", // "topleft", "bottomleft", "bottomright"
autoZIndex: true,
hideSingleBase: false,
sortLayers: false
}).addTo(map);
// Add default base map
osm.addTo(map);Important: The default base map must be added to the map explicitly. The layers control only shows the options — it doesn’t activate any base map by default.
Adding Layers to Control Dynamically
var newLayer = L.circle([52.52, 13.40], { radius: 3000 });
layersControl.addOverlay(newLayer, "New Layer");
// Remove from control
layersControl.removeLayer(newLayer);Image Overlays
Overlay an image on the map — useful for floor plans, historical maps, or heatmap renders:
var imageBounds = [
[40.712, -74.227], // Southwest corner
[40.774, -74.125] // Northeast corner
];
L.imageOverlay("map-image.jpg", imageBounds, {
opacity: 0.8,
alt: "Historical Map",
interactive: false, // Mouse events pass through to map below
crossOrigin: false,
errorOverlayUrl: "",
zIndex: 10
}).addTo(map);The image stretches to fit the coordinate bounds. If the image aspect ratio doesn’t match the bounds’ aspect ratio, it will distort.
Video Overlay
var videoBounds = [
[40.712, -74.227],
[40.774, -74.125]
];
L.videoOverlay("demo.mp4", videoBounds, {
opacity: 0.8,
interactive: false,
autoplay: true,
loop: true,
muted: true
}).addTo(map);This overlays a video on the map surface — great for dashboards showing live feeds or animated data visualizations.
Built-in Controls
Zoom Control
// Remove default zoom, replace with custom
map.removeControl(map.zoomControl);
L.control.zoom({
position: "topleft",
zoomInText: "+",
zoomOutText: "-",
zoomInTitle: "Zoom in",
zoomOutTitle: "Zoom out"
}).addTo(map);Scale Control
Shows a ruler indicating distance:
L.control.scale({
position: "bottomleft",
maxWidth: 200,
metric: true,
imperial: true,
updateWhenIdle: false
}).addTo(map);The scale automatically updates as you zoom in and out.
Custom Controls
When built-in controls aren’t enough, build your own:
var customControl = L.Control.extend({
options: {
position: "topleft"
},
onAdd: function(map) {
var container = L.DomUtil.create("div", "custom-control");
container.innerHTML = '<button style="padding:8px 12px;">Custom Button</button>';
// Prevent clicks on this control from panning the map
L.DomEvent.disableClickPropagation(container);
container.querySelector("button").onclick = function() {
alert("Custom control clicked!");
};
return container;
},
onRemove: function(map) {
// Cleanup when control is removed
}
});
map.addControl(new customControl({ position: "bottomright" }));disableClickPropagation is critical — without it, clicking your control also pans the map underneath.
Simplified Custom Control (Info Panel)
For simple controls, use this shorter pattern:
var info = L.control({ position: "bottomleft" });
info.onAdd = function(map) {
this._div = L.DomUtil.create("div", "info");
this._div.innerHTML = "<h4>Info Panel</h4>Click on the map";
L.DomEvent.disableClickPropagation(this._div);
return this._div;
};
info.update = function(props) {
this._div.innerHTML = "<h4>Info Panel</h4>" +
(props ? "<b>" + props.name + "</b>" : "Click on the map");
};
info.addTo(map);This pattern creates a panel that you can update later with info.update(data).
Managing Layers Programmatically
// Add/remove
map.addLayer(layer);
map.removeLayer(layer);
// Check existence
map.hasLayer(layer);
// Iterate all layers
map.eachLayer(function(layer) {
console.log(layer);
});
// Remove all non-tile layers
map.eachLayer(function(layer) {
if (layer instanceof L.TileLayer) return; // Skip base map
map.removeLayer(layer);
});Common Mistakes
1. Base maps not added to map
The layers control shows base map options, but you must explicitly add the initial base map: osm.addTo(map). Otherwise, the map starts blank.
2. Click events on custom controls pan the map
Always use L.DomEvent.disableClickPropagation(container) on custom control elements. Without it, every click on your control also pans the map.
3. Image overlay bounds don’t match image aspect ratio
If the coordinate bounds have a different aspect ratio than the image, the image stretches. Verify your bounds match the image dimensions.
4. Layer group events not firing
Layer groups don’t bubble events from child layers. Use L.featureGroup instead for event handling.
5. Forgetting to add overlay layers to the control
Overlays must exist before being passed to the layers control. If you create a layer after the control, use layersControl.addOverlay(layer, "Name") to add it dynamically.
Practice Questions
Q1: What is the difference between L.layerGroup and L.featureGroup?
A: FeatureGroup extends LayerGroup with event bubbling (clicks on children fire on the group) and setStyle() method for styling all children at once.
Q2: How do you add a default base map when using layers control?
A: Call .addTo(map) on the default tile layer: osm.addTo(map). The layers control only lists options; it doesn’t activate any.
Q3: What does L.DomEvent.disableClickPropagation() do?
A: Prevents mouse events on the element from propagating to the map, avoiding unwanted map interactions (panning, zooming) when clicking controls.
Q4: Can you have multiple base maps visible at the same time? A: Not with the standard layers control — base maps are mutually exclusive. For side-by-side comparison, use a swipe/spy plugin.
Q5: How do you remove all overlay layers at once?
A: Iterate with map.eachLayer(), skip L.TileLayer instances, and remove the rest.
Challenge: Build a map with three base maps (Street, Dark, Satellite), three overlay groups (Restaurants with markers, Parks with green polygons, Schools with blue circles), and a custom info panel that shows the clicked layer’s name.
FAQ
Try It Yourself
Here’s a complete layer switcher with base maps and overlays:
<!DOCTYPE html>
<html>
<head>
<title>Layer Switcher</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; }
</style>
</head>
<body>
<h2>Layer Switcher Demo</h2>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
var map = L.map("map").setView([48.85, 2.35], 5);
var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap"
});
var topo = L.tileLayer("https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", {
attribution: "© OpenTopMap"
});
var capitals = L.featureGroup([
L.marker([51.5, -0.09]).bindPopup("London"),
L.marker([48.85, 2.35]).bindPopup("Paris"),
L.marker([52.52, 13.40]).bindPopup("Berlin"),
L.marker([41.90, 12.50]).bindPopup("Rome"),
L.marker([40.42, -3.70]).bindPopup("Madrid")
]);
var radius = L.featureGroup([
L.circle([51.5, -0.09], { radius: 200000, color: "#e91e63", fillOpacity: 0.1 }),
L.circle([48.85, 2.35], { radius: 200000, color: "#9c27b0", fillOpacity: 0.1 }),
L.circle([52.52, 13.40], { radius: 200000, color: "#2196f3", fillOpacity: 0.1 })
]);
L.control.layers(
{ "OpenStreetMap": osm, "Topographic": topo },
{ "Capitals": capitals, "Radius Circles": radius },
{ collapsed: false }
).addTo(map);
osm.addTo(map);
capitals.addTo(map);
</script>
</body>
</html>What’s Next
| Lesson | Description |
|---|---|
| Events → | Click handlers, hover effects, and event propagation |
| Advanced & Plugins → | Heatmaps, routing, performance optimization |
| API Reference → | Complete cheatsheet and quick reference |
Related Topics: JavaScript event handling | HTML custom UI elements | CSS positioning controls
What’s Next
Congratulations on completing this Leafletjs Layers Controls 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