Pick Folium when the map only needs to display geometry — it renders finished HTML with zero widget dependencies; pick ipyleaflet when the map must return clicks, drawn shapes, or viewport bounds to Python, because its ipywidgets model gives you a live two-way channel that Folium’s static HTML cannot.

Jump to heading Why this matters

Both libraries wrap Leaflet, so a screenshot of either map looks identical. The difference is entirely in the plumbing behind the map, and that plumbing decides whether your Panel dashboard can react to what the user does. Folium serializes a map to a block of HTML and JavaScript that the browser renders as a finished artefact — there is no live Python handle afterward. ipyleaflet builds the map from ipywidgets, so the Map and every layer stay live Python objects whose traits sync to the browser in both directions. Choosing wrong means either fighting Folium to extract a click it was never designed to return, or paying for ipyleaflet’s per-session live model on a dashboard that only ever displays a static overlay. This choice sits underneath the whole ipyleaflet integration pathway and complements the static-Leaflet Folium & Leafmap integration guide.

Jump to heading Prerequisites

  • panel>=1.3; folium>=0.15 for the static path; ipyleaflet>=0.18 with ipywidgets>=8.0 for the live path.
  • geopandas>=0.14 and a source frame reprojected to EPSG:4326, which both libraries expect.
  • For ipyleaflet in a served app, pn.extension("ipywidgets") before any pane is built.

Jump to heading The core difference in one diagram

Folium one-way HTML versus ipyleaflet two-way commThe top row shows Folium: Python builds a map, serializes it to static HTML, and sends it once to the browser. A single arrow points from server to browser, and a crossed-out return arrow shows there is no channel back. The bottom row shows ipyleaflet: Python holds a live widget model connected to the browser Leaflet view by a bidirectional comm channel, so click, draw, and pan events flow back to Python callbacks.Folium — static HTML, one wayPythonbuild + serializeHTML stringfinished artefactBrowserrenders Leafletno return channel — click stays in the browseripyleaflet — live widget model, two wayPython widget modelobserve · on_clickBrowser Leaflet viewpan · click · drawcomm bridge

Jump to heading Comparison table

DimensionFoliumipyleaflet
Rendering modelStatic HTML/JS serialized onceLive ipywidgets model synced continuously
Bidirectional eventsNone natively — needs custom JS or a bridgeon_click, observe, on_draw deliver events to Python
Return click/viewport stateNot without extra JavaScriptBuilt in via synced traits
Panel embeddingpn.pane.HTML(m._repr_html_()) or pn.pane.plot.Foliumpn.panel(m) / pn.pane.IPyWidget(m)
Frontend dependencyNone beyond a browserRequires the widgets JS extension (pn.extension('ipywidgets'))
Large-layer performanceSerializes once; heavy GeoJSON bloats the HTML payloadData stays resident; re-serializes only changed traits
Server memory per sessionNegligible after renderGrows with layer size × concurrent sessions
Jupyter behaviourRenders inline as HTMLRenders as a live widget, fully interactive
Best fitReports, static overlays, quick prototypesReactive dashboards needing map-driven state

Jump to heading The same task in both libraries

Jump to heading Add a GeoJSON layer and handle a click — Folium

python
import folium
import geopandas as gpd
import panel as pn

gdf = gpd.read_file("data/nl_provinces.gpkg").to_crs("EPSG:4326")

m = folium.Map(location=[52.1326, 5.2913], zoom_start=7, tiles="CartoDB positron")
folium.GeoJson(
    gdf.__geo_interface__,
    name="provinces",
    tooltip=folium.GeoJsonTooltip(fields=["name"]),   # hover works client-side
).add_to(m)

# Panel can display it, but the click never returns to Python
map_pane = pn.pane.HTML(m._repr_html_(), sizing_mode="stretch_both", min_height=500)
map_pane.servable()
# A user click highlights the tooltip in the browser only — Python is not notified.

The click is invisible to Python. Folium’s GeoJsonTooltip renders entirely in the browser; to capture which province was clicked you would inject custom JavaScript that posts back through a hidden input or a streamlit-folium-style bridge.

Jump to heading Add a GeoJSON layer and handle a click — ipyleaflet

python
import geopandas as gpd
import panel as pn
from ipyleaflet import Map, GeoJSON, basemaps

pn.extension("ipywidgets")
gdf = gpd.read_file("data/nl_provinces.gpkg").to_crs("EPSG:4326")

m = Map(center=(52.1326, 5.2913), zoom=7, basemap=basemaps.CartoDB.Positron)
layer = GeoJSON(data=gdf.__geo_interface__, name="provinces")
m.add(layer)

clicked = pn.widgets.StaticText(name="Clicked", value="—")

def on_click(event, feature, **kwargs):
    clicked.value = feature["properties"]["name"]   # delivered straight to Python

layer.on_click(on_click)
pn.Row(clicked, pn.panel(m, min_height=500)).servable()

Here the clicked feature arrives in a Python callback with its full properties, and updating clicked.value flows through the rest of the reactive graph. That is the entire difference expressed in code.

Jump to heading Decision guidance

  • Choose Folium when the map is a display surface: a choropleth in a report, a static overlay, a quick prototype, or any view that never needs to return interaction state. You avoid the widget-extension dependency and keep near-zero per-session server memory.
  • Choose ipyleaflet when the map must drive the dashboard — clicks that filter a table, drawn polygons that run a spatial query, a viewport that triggers a cached fetch, or layers that swap reactively from a Panel widget. The two-way binding is worked through in syncing ipyleaflet layers with Panel widgets.
  • Mixed dashboards are legitimate: a static Folium overview in one tab, a live ipyleaflet analysis map in another. Just budget memory for the ipyleaflet sessions.

A useful way to make the call is to ask what the map’s output is. If the map’s only job is to render an input — a GeoDataFrame in, a picture out — Folium is the leaner tool and its lack of a return channel is not a limitation but a simplification. The moment the map has to produce an output that Python consumes — a selected id, a drawn boundary, a viewport extent that parameterises the next query — you need the live model, and retrofitting that onto Folium means reinventing a fraction of what ipyleaflet already gives you. Deciding this at design time, before any code is written, avoids the expensive mid-project rewrite where a “just display it” map grows an interactivity requirement it was never built to meet.

Jump to heading Verification

Confirm the behavioural difference rather than the appearance:

python
# ipyleaflet: the handler mutates Python state from a synthetic feature
sample = gdf.__geo_interface__["features"][0]
on_click(event="click", feature=sample)
assert clicked.value == sample["properties"]["name"], "ipyleaflet click did not reach Python"

# Folium: there is no equivalent callback to assert on — the HTML holds no Python hook
assert isinstance(m, folium.Map)  # only proves the object type, not interactivity
print("ipyleaflet returns click state to Python; Folium does not")

The asymmetry in what you can assert is the clearest statement of the trade-off: ipyleaflet exposes a Python-side event you can test, Folium does not.

Jump to heading Edge cases and gotchas

  • Folium cannot return click state without extra JavaScript. If a requirement says “clicking a feature updates a chart”, Folium alone will not satisfy it — plan for ipyleaflet or a JS bridge from the start.
  • ipyleaflet needs the widget extension. A served Panel app with ipyleaflet renders blank unless pn.extension("ipywidgets") runs before any pane is created. Folium has no such dependency.
  • Server memory under concurrency. ipyleaflet holds each session’s layer data live in the model, so 50 concurrent sessions each pinning a 5 MB layer is 250 MB of resident geometry. Folium serializes once and holds nothing, so pick it for high-concurrency read-only views, and see widget lifecycle management for keeping live models from multiplying across rerenders.

Jump to heading FAQ

Can Folium return which feature a user clicked in a Panel app?

Not on its own. Folium renders finished HTML with no live channel back to Python, so a click stays in the browser. You either inject custom JavaScript that posts the selection back through a bridge, or switch to ipyleaflet, whose on_click handler delivers the clicked GeoJSON feature straight to a Python callback.

Is ipyleaflet always the better choice for Panel?

No. If the map only displays geometry and never returns interaction state, Folium is simpler, carries no widget-extension dependency, and pins no per-session live model in server memory. ipyleaflet wins specifically when you need bidirectional events or reactive layer updates driven from Panel widgets.

Does ipyleaflet use more server memory than Folium?

Yes. ipyleaflet keeps a live widget model per session, so each layer’s serialized data stays resident for the life of the session. Folium serializes once to HTML and holds nothing afterward. Under high concurrency with large layers, ipyleaflet’s footprint grows with session count while Folium’s stays flat.


Back to ipyleaflet Integration

Related