TextMarker
Editable, draggable, styleable text placed on the map like a Marker — drag to move, double-click to edit, on-canvas resize/rotate handles, and a style toolbar; round-trips position/text/rotation/fontSize/color back to Dash.
---
.. llms_copy::TextMarker
.. toc::
Overview
dl2.TextMarker is editable, draggable, styleable text anchored to a [lat, lng] — used exactly like a dl2.Marker. Under the hood it is a Leaflet 2 Marker whose icon is a content-sized, optionally-contentEditable text box, so it pans and zooms with the basemap and behaves like a first-class map feature. It closes the "captions can't live on the map" gap from the text-caption-marker-proposal hand-off.
| Interaction | Result | |---|---| | Drag the label | moves it; writes position back (+ bumps n_drags) | | Double-click | inline-edit the text; commit on blur / Enter writes text back (+ n_edits) | | Resize handle (corner) | scales fontSize | | Rotate handle (top) | sets rotation (hold Shift to snap to 15°) | | Style toolbar (while selected) | font family / size, bold / italic, text & background color, rotation — each round-trips to Dash | | Click the label / empty map | toggles selected (two-way, so a host can drive selection) |
When position is omitted the label spawns at the center of the current viewport and writes that position back, so you can drop a caption with no coordinates and read where it landed.
The white anchor dot (which also doubles as the resize grip) is drawn at the chosen anchor — pick bottom and it sits at the bottom-center, top-left and it sits at the top-left, etc. — so you can always see where the label is pinned. center is special-cased to the bottom-right corner so the dot never covers the text.
Two size models via scaleWithZoom:
False(default) — a constant screen-size HUD caption:fontSizeis literal px at every
zoom (like a Tooltip).
True— geographic sizing: the on-screen size grows/shrinks by2^(zoom − referenceZoom),
so the caption keeps a fixed *ground* footprint as the camera flies (like a polygon's edge).
Live demo
Drag the "Fisherman's Wharf" caption, double-click to retype it, and use the on-canvas handles + glass toolbar — or drive every prop from the right column. The red "PIER 39" label has scaleWithZoom=True, so zoom in/out to watch it hold its ground size. The T tool in the top-right toolbar is the EditControl text tool (Route B): click it, click the map, and type — the caption is added to EditControl.geojson as a kind:"text" Point.
.. exec::docs.text-marker.example :code: false
Route B — the EditControl text tool
dl2.EditControl gains a text tool alongside marker / polyline / polygon / …. Picking it and clicking the map drops an inline-editable caption that participates in the same geojson round-trip as every other shape — a GeoJSON Point carrying the caption + style in properties:
``json { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-122.41, 37.808] }, "properties": { "kind": "text", "text": "Fisherman's Wharf", "color": "#111827", "fontSize": 18, "fontFamily": "system-ui, sans-serif", "fontWeight": 600 } } ``
In edit mode the caption is draggable and double-click re-opens the inline editor. Enable it per-tool with draw={"text": True} (and disable the others to get a text-only toolbar).
Source
```python
File: docs/text-marker/example.py
""" TextMarker — editable, draggable, styleable map captions (limited working example).
The map hosts one selected dl2.TextMarker you can drag, double-click to edit, and restyle with the on-canvas resize / rotate handles + the contextual toolbar — OR drive every prop from the right column. A second caption has scaleWithZoom=True so it keeps a fixed ground footprint as you zoom. The EditControl's text tool (Route B) is wired in too: pick the T tool, click the map, and type — the caption round-trips through the same geojson channel as every drawn shape, as a kind:"text" Point. """
import dash_leaflet2 as dl2 import dash_mantine_components as dmc from dash import Input, Output, callback, clientside_callback from dl2_tiles import SATELLITE, register_theme_swap from dl2_locations import SAN_FRANCISCO from dl2_shared import code_panel, header, info_panel
Basemap pair for this page. dl2_tiles owns the light/dark wiring so
every example themes the same way — see register_theme_swap below.
TILES = SATELLITE TILE_URL = TILES.url("light") ATTR = TILES.attribution()
CENTER = SAN_FRANCISCO.at(1.4, -1.3) # Fisherman's Wharf
ANCHORS = [ "top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right", ]
CODE = """import dash_leaflet2 as dl2
dl2.Map(center=[37.808, -122.409], zoom=14, children=[ dl2.TileLayer(),
# A caption you place like a Marker: drag to move, double-click to edit, # and (when selected) resize / rotate with the on-canvas handles. dl2.TextMarker( id="cap", text="Fisherman's Wharf", position=[37.808, -122.409], color="#0b3d66", fontSize=26, fontWeight=700, backgroundColor="rgba(255,255,255,0.6)", selected=True, # shows the handles + style toolbar ),
# scaleWithZoom keeps a fixed GROUND size (grows on screen as you zoom in). dl2.TextMarker(text="PIER 39", position=[37.8087, -122.4098], color="#c92a2a", fontSize=16, scaleWithZoom=True),
# Route B — the EditControl 'text' tool: click the T, click the map, type. # Captions serialize as kind:"text" Point features in EditControl.geojson. dl2.EditControl(id="edit", draw={"text": True}), ])
read it back
@callback(Output("out", "children"), Input("cap", "position"), Input("cap", "text"), Input("cap", "n_edits")) def show(pos, text, n_edits): ... """
def _map(): return dl2.Map( id="tm-map", center=CENTER, zoom=14, style={"height": "62vh"}, children=[ dl2.TileLayer(id="tm-tile", url=TILE_URL, attribution=ATTR), dl2.TextMarker( id="tm-cap", text="Fisherman's Wharf", position=CENTER, color="#0b3d66", fontSize=26, fontWeight=700, backgroundColor="rgba(255,255,255,0.6)", anchor="center", selected=True, ), dl2.TextMarker( id="tm-geo", text="PIER 39", position=[37.8087, -122.4098], color="#c92a2a", fontSize=16, fontWeight=700, scaleWithZoom=True, ), dl2.EditControl( id="tm-edit", position="topright", # Only expose the text tool here to keep the demo focused. draw={ "marker": False, "polyline": False, "polygon": False, "rectangle": False, "circle": False, "circlemarker": False, "text": True, }, ), ], )
component = dmc.Stack( [ header( "TextMarker — editable map captions", "Place styled text on the map like a Marker. Drag to move, double-click to " "edit, resize/rotate with the on-canvas handles, and restyle from the glass " "toolbar — or drive every prop from the right. Position, text, rotation, font " "size, and color all round-trip back to Dash.", badge="dl2.TextMarker · EditControl text tool", ), dmc.Grid( [ dmc.GridCol( dmc.Paper( _map(), shadow="sm", radius="md", withBorder=True, style={"overflow": "hidden", "height": "62vh"}, ), span=8, ), dmc.GridCol( dmc.Stack( [ info_panel( "Caption text", dmc.TextInput(id="tm-text", value="Fisherman's Wharf"), ), info_panel( "Typography", dmc.Stack( [ dmc.ColorInput(id="tm-color", value="#0b3d66", format="hex"), dmc.Text("Font size", size="xs", c="dimmed"), dmc.Slider(id="tm-size", min=10, max=64, value=26), dmc.Text("Rotation", size="xs", c="dimmed"), dmc.Slider( id="tm-rot", min=-180, max=180, value=0, marks=[{"value": 0, "label": "0°"}], ), ], gap="xs", ), ), info_panel( "Anchor", dmc.SegmentedControl( id="tm-anchor", data=[{"label": a, "value": a} for a in ANCHORS], value="center", orientation="vertical", fullWidth=True, size="xs", ), ), info_panel( "Geographic sizing", dmc.Switch( id="tm-scale", label="scaleWithZoom (fixed ground size)", checked=False, ), ), info_panel( "Live readback", dmc.Code(id="tm-out", block=True, children="…"), ), ], gap="md", ), span=4, ), ], gutter="md", ), code_panel("A caption that places, styles, and round-trips like a Marker", CODE), ], gap="md", )
---- controls -> component (one-way drivers; the on-map UI is the other path) ----------
@callback(Output("tm-cap", "text"), Input("tm-text", "value")) def _text(v): return v or ""
@callback(Output("tm-cap", "color"), Input("tm-color", "value")) def _color(v): return v or "#0b3d66"
@callback(Output("tm-cap", "fontSize"), Input("tm-size", "value")) def _size(v): return int(v or 26)
@callback(Output("tm-cap", "rotation"), Input("tm-rot", "value")) def _rot(v): return int(v or 0)
@callback(Output("tm-cap", "anchor"), Input("tm-anchor", "value")) def _anchor(v): return v or "center"
@callback(Output("tm-cap", "scaleWithZoom"), Input("tm-scale", "checked")) def _scale(checked): return bool(checked)
---- component -> readback panel -------------------------------------------------------
@callback( Output("tm-out", "children"), Input("tm-cap", "position"), Input("tm-cap", "text"), Input("tm-cap", "rotation"), Input("tm-cap", "fontSize"), Input("tm-cap", "n_edits"), Input("tm-cap", "n_drags"), Input("tm-edit", "geojson"), ) def _readback(pos, text, rot, size, n_edits, n_drags, geo): n_captions = sum( 1 for f in (geo or {}).get("features", []) if (f.get("properties") or {}).get("kind") == "text" ) return ( f"position : {pos}\n" f"text : {text!r}\n" f"rotation : {rot}\n" f"fontSize : {size}\n" f"n_edits : {n_edits or 0}\n" f"n_drags : {n_drags or 0}\n" f"edit-tool captions: {n_captions}" )
---- light/dark tile swap (standard pattern) ------------------------------------------
register_theme_swap("tm-tile", TILES) ```
:defaultExpanded: false :withExpandedButton: true
---
*Source: /text-marker*
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:
- /text-marker/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt