Part of the Layer Management & Toggling guide.
Operative rule: Add folium.LayerControl() last, after every overlay is on the map — it captures only the layers that already exist when it is created, so anything added afterward will be missing from the toggle.
How Folium LayerControl Works
Folium models a map as a tree of children. Each tile layer, folium.FeatureGroup, and folium.GeoJson you add becomes a named child of the map, and each carries two flags that decide how it behaves in the switcher: overlay (checkbox versus radio) and show (initially visible or not). folium.LayerControl is not a container you register layers into; it is a Leaflet control that, at the moment you add it, walks the map’s existing children and builds a toggle entry for every one that declares a name. This is why ordering is load-bearing — the control is a snapshot, not a live subscription. Add an overlay after the control and the layer renders on the map but never appears in the switcher, because the snapshot was already taken. This single ordering constraint is the heart of Layer Management & Toggling in Folium.
The two flags compose cleanly. overlay=True makes the layer a checkbox so multiple data layers can be visible simultaneously; overlay=False places it in the base-layer group as a radio button, where selecting one deselects the rest — the right model for mutually exclusive base maps. show=False registers the layer but leaves it switched off on load, which keeps a busy dashboard legible while still offering the data a click away. When the overlays come from a geopandas GeoDataFrame, you group rows by an attribute, build one named FeatureGroup per group, and add each GeoJson into its group — a pattern that pairs naturally with safely embedding the resulting Folium map in React dashboards. Because PyDeck has no equivalent built-in, the same requirement drives a hand-rolled solution when building a custom layer switcher in PyDeck.
Production-Ready Implementation
The script loads a geopandas GeoDataFrame, splits it into named overlays by a category column, styles each group, and — critically — adds folium.LayerControl() only after the loop has registered every group. Base tiles are added with overlay=False so they behave as radio-selectable base maps rather than checkboxes.
from __future__ import annotations
import folium
import geopandas as gpd
# Distinct fill colour per category; keys must match the "category" values.
CATEGORY_STYLE: dict[str, str] = {
"residential": "#4e79a7",
"commercial": "#59a14f",
"industrial": "#e15759",
}
def build_toggleable_map(gdf: gpd.GeoDataFrame) -> folium.Map:
"""Build a Folium map with one toggleable overlay per category."""
# LayerControl and GeoJson both require EPSG:4326 (WGS84 lon/lat).
gdf = gdf.to_crs("EPSG:4326")
centroid = gdf.geometry.union_all().centroid
m = folium.Map(location=[centroid.y, centroid.x], zoom_start=12, tiles=None)
# Base layers: overlay=False -> radio buttons in the base-layer group.
folium.TileLayer("CartoDB positron", name="Light base", overlay=False).add_to(m)
folium.TileLayer("CartoDB dark_matter", name="Dark base", overlay=False,
show=False).add_to(m)
# One named FeatureGroup per category. Build ALL of them before the control.
for category, group in gdf.groupby("category"):
colour = CATEGORY_STYLE.get(category, "#888888")
# show=False on industrial keeps the initial view uncluttered.
fg = folium.FeatureGroup(
name=f"{category.title()} parcels",
overlay=True, # checkbox -> multiple can be visible
show=(category != "industrial"),
)
folium.GeoJson(
group.to_json(),
style_function=lambda _feat, c=colour: {
"fillColor": c,
"color": c,
"weight": 1,
"fillOpacity": 0.4,
},
tooltip=folium.GeoJsonTooltip(fields=["name", "category"]),
).add_to(fg)
fg.add_to(m)
# OPERATIVE RULE: add the control LAST, after every overlay exists.
folium.LayerControl(collapsed=False).add_to(m)
return m
if __name__ == "__main__":
gdf = gpd.read_file("parcels.geojson") # must contain a "category" column
build_toggleable_map(gdf).save("parcels_map.html")
Alternative Variants
Grouping overlays with GroupedLayerControl
When several overlays are mutually exclusive within a theme — pick exactly one demographic layer at a time — the plugin folium.plugins.GroupedLayerControl renders radio groups over FeatureGroups. The groups must reference layers already added to the map, so the same last-added ordering rule applies.
from folium.plugins import GroupedLayerControl
pop = folium.FeatureGroup(name="Population", show=True).add_to(m)
income = folium.FeatureGroup(name="Median income", show=False).add_to(m)
# Radio group: only one of these two overlays is visible at a time.
GroupedLayerControl(
groups={"Demographic layer": [pop, income]},
exclusive_groups=True,
collapsed=False,
).add_to(m) # still added after the FeatureGroups it references
Flag reference for overlays
| Argument | Effect | Typical use |
|---|---|---|
name="…" |
Label shown in the control | Every toggleable layer needs one |
overlay=True |
Checkbox; stackable | Data overlays (parcels, incidents) |
overlay=False |
Radio in base group | Mutually exclusive base tiles |
show=True |
Visible on load | Primary layer |
show=False |
Registered but off | Secondary/heavy layers |
control=False |
Rendered, hidden from control | Always-on context layers |
Verification Steps
- Every overlay appears: open the saved
.htmland confirm the control lists each named layer; a missing entry almost always means the layer was added afterLayerControl. - Initial state matches flags: confirm
show=Falselayers load unchecked andshow=Truelayers load checked. - Base layers are exclusive: confirm the base tiles behave as radio buttons — selecting one hides the other — while data overlays remain independent checkboxes.
- Toggling redraws cleanly: switch each overlay off and on and confirm features disappear and reappear without stale geometry or console errors.
- CRS is correct: confirm overlays align with the base tiles; misalignment signals the
GeoDataFramewas not reprojected toEPSG:4326.
Common Errors & Fixes
LayerControl is empty or missing layers
folium.LayerControl() was added before some (or all) overlays, so its snapshot captured nothing or only part of the tree. Fix: move the folium.LayerControl() call to the very end of the build, after every FeatureGroup and GeoJson has been added. This is the operative rule; there is no argument that makes the control retroactively pick up later layers.
Overlays render but cannot be turned off
The layers were added to the map without a name, so LayerControl skipped them even though they draw. An unnamed layer is invisible to the control by design. Fix: pass an explicit name="…" to each FeatureGroup, GeoJson, or TileLayer you want in the toggle, and add data features inside a named FeatureGroup rather than directly on the map.
Base maps show as checkboxes and can all be switched off at once
The tile layers were added with the default overlay=True, so they behave as independent overlays and the user can hide every base map, leaving a blank canvas. Fix: add base tiles with overlay=False so they join the radio-selectable base group, where exactly one is always active.
GeoJson overlay is offset from the base tiles
The source GeoDataFrame is in a projected CRS (a UTM zone or national grid) rather than WGS84, so Leaflet places the features in the wrong location. Fix: call gdf.to_crs("EPSG:4326") before building any GeoJson, since Leaflet and the GeoJSON spec both assume EPSG:4326 lon/lat.
Related
- Layer Management & Toggling — parent guide covering overlay strategy across Folium, Leaflet, and PyDeck
- Building a Custom Layer Switcher in PyDeck — the hand-rolled equivalent for a renderer with no built-in control
- Safely Embedding Folium Maps in React Dashboards — placing a toggleable Folium map inside an isolated frame
- Exporting Folium Maps to Static HTML with Embedded Assets — shipping the toggle-enabled map as a single self-contained file