Leaflet2

Leaflet 2-native map components for Dash — Map, TileLayer, Marker, GeoJSON, LayersControl and more, with no react-leaflet dependency.

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

Visit GitHub Repo

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:

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:

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 = (
    '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> '
    '&copy; <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

PropertyTypeDefaultDescription
idstring-Component ID for Dash callbacks.
childrennode-Child layers (TileLayer, Marker, ...) rendered into this map.
center[number, number][51.505, -0.09]Initial map center as [lat, lng]. [MUTABLE]
zoomnumber13Initial zoom level. [MUTABLE]
viewportdict-Current view state, written back on every moveend/zoomend: {center, zoom, bearing, bounds: {north, south, east, west}}. [READONLY]
clickDatadict-Most recent map click: {latlng: [lat, lng]}. [READONLY]
flyTodict-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]
minZoomnumber-Minimum zoom the user can zoom out to (most restrictive of Map/TileLayer wins). [MUTABLE]
maxZoomnumber-Maximum zoom the user can zoom in to (smallest of Map/TileLayer wins). [MUTABLE]
bearingnumber-Map rotation in degrees (CSS-based; keep 0 when drawing interactively). [MUTABLE]
draggingbooleanTrueMouse / pointer drag panning. [MUTABLE]
scrollWheelZoombooleanTrueMouse-wheel zoom. [MUTABLE]
doubleClickZoombooleanTrueDouble-click-to-zoom. [MUTABLE]
boxZoombooleanTrueShift-drag box-zoom selection. [MUTABLE]
pinchZoombooleanTruePinch-to-zoom on touch devices (v1's touchZoom). [MUTABLE]
keyboardbooleanTruePan/zoom with arrow keys and +/-. [MUTABLE]
tapHoldboolean-Mobile-Safari tap-hold-to-contextmenu emulation. [MUTABLE]
n_movestartnumber-Counter bumped on every movestart. [READONLY]
n_moveendnumber-Counter bumped on every moveend. [READONLY]
preferCanvasbooleanFalseRender all vector layers through the Canvas renderer.
zoomControlbooleanTrueBuilt-in +/- zoom buttons (constructor-only).
attributionControlbooleanTrueBuilt-in attribution control; set False to mount your own dl2.AttributionControl. Constructor-only.
className / stylestring / object-CSS class / inline styles. Set the map height via style.

TileLayer Props

PropertyTypeDefaultDescription
idstring-Component ID for Dash callbacks.
urlstringOSM tilesTile URL template, e.g. https://tile.openstreetmap.org/{z}/{x}/{y}.png. Updating it swaps the basemap. [MUTABLE]
attributionstring© OpenStreetMap contributorsAttribution HTML shown on the map.
opacitynumber1Layer opacity, 0..1. [MUTABLE]
minZoomnumber0Minimum zoom at which this layer is visible — below it Leaflet stops requesting tiles entirely.
maxZoomnumber19Maximum zoom level for this tile layer.
maxNativeZoomnumber-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]].
errorTileUrlstring-Image shown in place of a tile that fails to load (a 1x1 transparent PNG data URL hides broken tiles).
zIndexnumber-Explicit z-index for the layer's DOM pane when stacking tile layers. [MUTABLE]
subdomainsstring or list-Subdomains substituted into the {s} placeholder — ['a','b','c'] or 'abc'.
detectRetinabooleanFalseRequest 2x-resolution tiles on hi-DPI displays.
tmsbooleanFalseInvert Y coordinates for TMS-shaped tile pyramids.
crossOriginstring-crossOrigin attribute on tile img elements; pass "anonymous" for canvas-readable tiles. Construction-time only.
classNamestring-CSS class name(s).

New in 0.1.0 (pro-parity props): TileLayer gained minZoom, bounds, errorTileUrl, zIndex, subdomains, detectRetina, and tms; Map gained maxBounds plus 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: