Full documentation: https://leaflet.2plot.dev — the dedicated dash-leaflet2 documentation site, with the complete API reference and deeper examples. This page is the quick-start overview.
Installation
pip install dash-leaflet2
Introduction
dash-leaflet2 is a Leaflet 2-native rewrite of Dash mapping components. The original dash-leaflet is frozen on react-leaflet, which is built around Leaflet 1.9's context/lifecycle model and has no Leaflet 2 line. dash-leaflet2 wraps Leaflet 2 core directly (no react-leaflet) via a tiny React-context bridge, targeting Dash 4 and working toward dash-leaflet 1.x feature parity.
Shedding the react-leaflet abstraction layer unlocks Leaflet 2's headline features:
- Unified Pointer Events — one event model for mouse, touch, and pen.
ResizeObserver-based sizing — no more gray tiles when a map lives inside tabs or accordions.- ES6-class subclassing and canvas/WebGL
BlanketOverlaylayers. - Bundled everything — Leaflet 2, marker images, iconify-icon, and a liquid-glass theme ship inside
dash_leaflet2.js. No CDN, no JS build step at install time.
Version 0.1.0 ships 26 components: Map, TileLayer, Marker, Popup, Tooltip, LayersControl, BaseLayer, Overlay, GeoJSON (with SuperCluster clustering), EditControl, FeatureGroup, LayerGroup, Circle, CircleMarker, Polygon, Polyline, Rectangle, ImageOverlay, MiniMap, ScaleControl, FullScreenControl, AttributionControl, KeyboardControl, TextMarker, TileSelector, and EasyButton.
import dash_leaflet2 as dl2
Status: alpha, tracking Leaflet
2.0.0-alpha.1. APIs may change.
Quick Start
The core trio — dl2.Map + dl2.TileLayer + dl2.Marker — with a rich dl2.Popup (any Dash component works as popup content). Markers support four icon modes: the default pin, a custom image (icon), any emoji, or 200k+ Iconify icons (iconify).
Important: always give the Map an explicit height via style, e.g. style={"height": "500px", "width": "100%"}.
# File: docs/dash_leaflet2/quick_start.py
import dash_leaflet2 as dl2
import dash_mantine_components as dmc
from dash import html
CENTER = [28.0206, -97.0544]
component = dmc.Paper(
withBorder=True,
p="md",
children=[
dmc.Stack(
[
dmc.Text("Map + TileLayer + Marker + Popup", fw=500),
dmc.Text(
"The core trio: a Map with an OpenStreetMap TileLayer and a draggable "
"Marker hosting a rich Popup. Drag the marker — its position round-trips "
"back to Dash.",
size="sm",
c="dimmed",
),
dl2.Map(
id="leaflet2-quickstart-map",
center=CENTER,
zoom=12,
style={"height": "500px", "width": "100%", "borderRadius": "8px", "overflow": "hidden"},
children=[
dl2.TileLayer(),
dl2.Marker(
id="leaflet2-quickstart-marker",
position=CENTER,
draggable=True,
children=[
dl2.Tooltip(children="Drag me!"),
dl2.Popup(
children=html.Div(
[
html.B("dl2.Marker + dl2.Popup"),
html.Br(),
"Any Dash component works as popup content — "
"it renders through a React portal.",
]
),
maxWidth=260,
),
],
),
dl2.Marker(
id="leaflet2-quickstart-emoji",
position=[28.045, -97.02],
emoji="🛥️",
iconSize=34,
popup="Emoji markers work out of the box.",
),
dl2.Marker(
id="leaflet2-quickstart-iconify",
position=[28.0, -97.09],
iconify="mdi:lighthouse-on",
iconColor="orange",
iconSize=36,
popup="200k+ Iconify icons, lazy-loaded from the Iconify API.",
),
],
),
],
gap="sm",
)
],
)
Viewport & Interaction Callbacks
The map writes state back to Dash through read-only props:
viewport— updated on everymoveend/zoomendas{center: [lat, lng], zoom, bearing, bounds: {north, south, east, west}}.clickData— the most recent map click as{latlng: [lat, lng]}.n_movestart/n_moveend— counters for pan/fly transitions (drive a "flying…" indicator).
Draggable markers write their new position back too, and the flyTo prop goes the other way — trigger smooth flyTo / setView / panTo / fitBounds transitions from Python callbacks.
# File: docs/dash_leaflet2/interactions.py
import dash_leaflet2 as dl2
import dash_mantine_components as dmc
from dash import callback, Input, Output
CENTER = [28.0206, -97.0544]
component = dmc.Paper(
withBorder=True,
p="md",
children=[
dmc.Stack(
[
dmc.Text("Viewport + click events in Python callbacks", fw=500),
dmc.Text(
"The Map writes its view state back to Dash on every moveend/zoomend "
"via the read-only `viewport` prop, and the last click via `clickData`. "
"Pan, zoom, and click the map.",
size="sm",
c="dimmed",
),
dmc.Grid(
[
dmc.GridCol(
dl2.Map(
id="leaflet2-interactions-map",
center=CENTER,
zoom=11,
style={"height": "500px", "width": "100%", "borderRadius": "8px", "overflow": "hidden"},
children=[
dl2.TileLayer(),
dl2.Marker(
id="leaflet2-interactions-marker",
position=CENTER,
draggable=True,
tooltip="Drag me — position echoes below",
),
],
),
span={"base": 12, "md": 8},
),
dmc.GridCol(
dmc.Stack(
[
dmc.Text("Viewport (moveend / zoomend)", size="sm", fw=500),
dmc.Code(
"pan or zoom the map…",
id="leaflet2-interactions-viewport",
block=True,
style={"minHeight": "130px"},
),
dmc.Text("Last map click", size="sm", fw=500),
dmc.Code("click the map…", id="leaflet2-interactions-click", block=True),
dmc.Text("Marker position", size="sm", fw=500),
dmc.Code("drag the marker…", id="leaflet2-interactions-position", block=True),
],
gap="xs",
),
span={"base": 12, "md": 4},
),
]
),
],
gap="sm",
)
],
)
@callback(
Output("leaflet2-interactions-viewport", "children"),
Input("leaflet2-interactions-map", "viewport"),
prevent_initial_call=True,
)
def leaflet2_show_viewport(vp):
if not vp:
return "pan or zoom the map…"
b = vp["bounds"]
return (
f"center: {vp['center'][0]:.4f}, {vp['center'][1]:.4f}\n"
f"zoom: {vp['zoom']}\n"
f"bounds: N {b['north']:.3f} S {b['south']:.3f}\n"
f" E {b['east']:.3f} W {b['west']:.3f}"
)
@callback(
Output("leaflet2-interactions-click", "children"),
Input("leaflet2-interactions-map", "clickData"),
prevent_initial_call=True,
)
def leaflet2_show_click(click_data):
if not click_data:
return "click the map…"
lat, lng = click_data["latlng"]
return f"{lat:.5f}, {lng:.5f}"
@callback(
Output("leaflet2-interactions-position", "children"),
Input("leaflet2-interactions-marker", "position"),
prevent_initial_call=True,
)
def leaflet2_show_position(position):
if not position:
return "drag the marker…"
return f"{position[0]:.5f}, {position[1]:.5f}"
LayersControl & GeoJSON
dl2.LayersControl renders Leaflet's layer picker: dl2.BaseLayer children register as mutually-exclusive base maps (radio), dl2.Overlay children as independent toggles (checkbox). Both activeBase and activeOverlays are two-way — they reflect user choices in callbacks and accept updates from Python.
dl2.GeoJSON renders a FeatureCollection from the data prop, reports the clicked feature's properties via clickFeature, and supports SuperCluster point clustering with cluster=True plus pointToLayer / clusterToLayer JS hooks and a hideout passthrough for styling without Python round-trips.
# File: docs/dash_leaflet2/layers_geojson.py
import dash_leaflet2 as dl2
import dash_mantine_components as dmc
from dash import callback, Input, Output
CENTER = [28.02, -97.05]
OSM = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
CARTO_LIGHT = "https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
CARTO_DARK = "https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"
CARTO_ATTR = (
'© <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> '
'© <a href="https://carto.com/attributions">CARTO</a>'
)
SENSORS = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {"name": "Buoy 1"},
"geometry": {"type": "Point", "coordinates": [-97.04, 28.04]},
},
{
"type": "Feature",
"properties": {"name": "Buoy 2"},
"geometry": {"type": "Point", "coordinates": [-97.08, 28.01]},
},
{
"type": "Feature",
"properties": {"name": "Buoy 3"},
"geometry": {"type": "Point", "coordinates": [-97.06, 28.06]},
},
],
}
component = dmc.Paper(
withBorder=True,
p="md",
children=[
dmc.Stack(
[
dmc.Text("LayersControl + GeoJSON", fw=500),
dmc.Text(
"Base layers are mutually exclusive radios; overlays are independent "
"checkboxes. activeBase and activeOverlays are two-way props — the control "
"reports user choices to Python and accepts them from callbacks. Click a "
"GeoJSON point to read its feature properties.",
size="sm",
c="dimmed",
),
dl2.Map(
id="leaflet2-layers-map",
center=CENTER,
zoom=12,
style={"height": "500px", "width": "100%", "borderRadius": "8px", "overflow": "hidden"},
children=[
dl2.LayersControl(
id="leaflet2-layers-control",
position="topright",
children=[
dl2.BaseLayer(
dl2.TileLayer(url=CARTO_LIGHT, attribution=CARTO_ATTR),
name="Light",
checked=True,
),
dl2.BaseLayer(
dl2.TileLayer(url=CARTO_DARK, attribution=CARTO_ATTR),
name="Dark",
),
dl2.BaseLayer(
dl2.TileLayer(url=OSM),
name="OSM",
),
dl2.Overlay(
dl2.Polygon(
positions=[
[28.05, -97.10],
[28.06, -97.02],
[28.01, -97.00],
[28.00, -97.08],
],
color="teal",
fillOpacity=0.2,
children=dl2.Tooltip(children="Harbor zone"),
),
name="Harbor zone",
checked=True,
),
dl2.Overlay(
dl2.GeoJSON(
id="leaflet2-layers-geojson",
data=SENSORS,
),
name="Sensors",
checked=True,
),
],
),
],
),
dmc.Group(
[
dmc.Text("Active layers:", size="sm", fw=500),
dmc.Code("—", id="leaflet2-layers-active"),
dmc.Text("Clicked feature:", size="sm", fw=500),
dmc.Code("click a sensor point…", id="leaflet2-layers-feature"),
],
gap="xs",
),
],
gap="sm",
)
],
)
@callback(
Output("leaflet2-layers-active", "children"),
Input("leaflet2-layers-control", "activeBase"),
Input("leaflet2-layers-control", "activeOverlays"),
)
def leaflet2_show_active_layers(base, overlays):
return f"base: {base} | overlays: {overlays}"
@callback(
Output("leaflet2-layers-feature", "children"),
Input("leaflet2-layers-geojson", "clickFeature"),
prevent_initial_call=True,
)
def leaflet2_show_clicked_feature(props):
if not props:
return "click a sensor point…"
return str(props)
Component Properties
Props tagged [MUTABLE] accept updates from callbacks (Python → map); [READONLY] props are written back by the map (map → Python). Full prop lists live in each generated class's docstring — help(dl2.Map).
Map Props
| Property | Type | Default | Description | |||||
|---|---|---|---|---|---|---|---|---|
id | string | - | Component ID for Dash callbacks. | |||||
children | node | - | Child layers (TileLayer, Marker, ...) rendered into this map. | |||||
center | [number, number] | [51.505, -0.09] | Initial map center as [lat, lng]. [MUTABLE] | |||||
zoom | number | 13 | Initial zoom level. [MUTABLE] | |||||
viewport | dict | - | Current view state, written back on every moveend/zoomend: {center, zoom, bearing, bounds: {north, south, east, west}}. [READONLY] | |||||
clickData | dict | - | Most recent map click: {latlng: [lat, lng]}. [READONLY] | |||||
flyTo | dict | - | Python → map viewport transition: `{transition: 'flyTo'\ | 'setView'\ | 'panTo'\ | 'fitBounds'\ | 'flyToBounds'\ | 'panInsideBounds', center?, zoom?, bounds?, options?, n_clicks}`. [MUTABLE] |
maxBounds | [[number, number], [number, number]] | - | Geographic bounds the view is constrained inside, as [[south, west], [north, east]]. [MUTABLE] | |||||
minZoom | number | - | Minimum zoom the user can zoom out to (most restrictive of Map/TileLayer wins). [MUTABLE] | |||||
maxZoom | number | - | Maximum zoom the user can zoom in to (smallest of Map/TileLayer wins). [MUTABLE] | |||||
bearing | number | - | Map rotation in degrees (CSS-based; keep 0 when drawing interactively). [MUTABLE] | |||||
dragging | boolean | True | Mouse / pointer drag panning. [MUTABLE] | |||||
scrollWheelZoom | boolean | True | Mouse-wheel zoom. [MUTABLE] | |||||
doubleClickZoom | boolean | True | Double-click-to-zoom. [MUTABLE] | |||||
boxZoom | boolean | True | Shift-drag box-zoom selection. [MUTABLE] | |||||
pinchZoom | boolean | True | Pinch-to-zoom on touch devices (v1's touchZoom). [MUTABLE] | |||||
keyboard | boolean | True | Pan/zoom with arrow keys and +/-. [MUTABLE] | |||||
tapHold | boolean | - | Mobile-Safari tap-hold-to-contextmenu emulation. [MUTABLE] | |||||
n_movestart | number | - | Counter bumped on every movestart. [READONLY] | |||||
n_moveend | number | - | Counter bumped on every moveend. [READONLY] | |||||
preferCanvas | boolean | False | Render all vector layers through the Canvas renderer. | |||||
zoomControl | boolean | True | Built-in +/- zoom buttons (constructor-only). | |||||
attributionControl | boolean | True | Built-in attribution control; set False to mount your own dl2.AttributionControl. Constructor-only. | |||||
className / style | string / object | - | CSS class / inline styles. Set the map height via style. |
TileLayer Props
| Property | Type | Default | Description |
|---|---|---|---|
id | string | - | Component ID for Dash callbacks. |
url | string | OSM tiles | Tile URL template, e.g. https://tile.openstreetmap.org/{z}/{x}/{y}.png. Updating it swaps the basemap. [MUTABLE] |
attribution | string | © OpenStreetMap contributors | Attribution HTML shown on the map. |
opacity | number | 1 | Layer opacity, 0..1. [MUTABLE] |
minZoom | number | 0 | Minimum zoom at which this layer is visible — below it Leaflet stops requesting tiles entirely. |
maxZoom | number | 19 | Maximum zoom level for this tile layer. |
maxNativeZoom | number | - | Max zoom the tile source actually has tiles for; Leaflet upscales past it instead of 404-ing. |
bounds | [[number, number], [number, number]] | - | Bounds outside of which no tiles are requested, as [[south, west], [north, east]]. |
errorTileUrl | string | - | Image shown in place of a tile that fails to load (a 1x1 transparent PNG data URL hides broken tiles). |
zIndex | number | - | Explicit z-index for the layer's DOM pane when stacking tile layers. [MUTABLE] |
subdomains | string or list | - | Subdomains substituted into the {s} placeholder — ['a','b','c'] or 'abc'. |
detectRetina | boolean | False | Request 2x-resolution tiles on hi-DPI displays. |
tms | boolean | False | Invert Y coordinates for TMS-shaped tile pyramids. |
crossOrigin | string | - | crossOrigin attribute on tile img elements; pass "anonymous" for canvas-readable tiles. Construction-time only. |
className | string | - | CSS class name(s). |
New in 0.1.0 (pro-parity props):
TileLayergainedminZoom,bounds,errorTileUrl,zIndex,subdomains,detectRetina, andtms;MapgainedmaxBoundsplus the full set of interaction handler toggles (dragging,scrollWheelZoom,doubleClickZoom,boxZoom,pinchZoom,keyboard,tapHold).
Beyond the Basics
The library also ships dl2.EditControl (native Leaflet 2 draw/edit with GeoJSON round-trip — no leaflet-draw), dl2.MiniMap, dl2.ScaleControl, dl2.FullScreenControl, dl2.TextMarker, dl2.EasyButton (Iconify-icon buttons with n_clicks), and dl2.TileSelector (hover-highlight, click-to-toggle tile selection). See the GitHub repository for the full showcase.
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /pip/dash_leaflet2/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt