Easy Button

Easy Button + marker creation flow with emoji picker, form popup, and view mode.

Easy Button

Easy Button + marker creation flow with emoji picker, form popup, and view mode.

---

.. llms_copy::Easy Button

.. toc::

Overview

Click the smile button on the map (top-left) to enter create mode → a dmc.Popover opens directly to the right of the button with a DashEmojiMart picker. Click the map to place the marker; a form popup opens above it with name/type fields. Pick an emoji at any time to update the marker's icon live. Create finalizes the marker (pushes to a markers store, exits create mode, closes everything); Cancel discards.

(We use dmc.Popover, not dmc.HoverCard. HoverCard is hover-triggered only — it would close the moment the user moves their mouse off the button toward the map to place a marker. Popover supports click-triggered + controlled opened, same visual styling.)

Live demo

.. exec::docs.easy-button.example :code: false

Source

```python

File: docs/easy-button/example.py

""" Easy Button + marker creation flow with emoji picker, form popup, and view mode.

Click the smile button on the map (top-left) to enter create mode → a dmc.Popover opens directly to the right of the button with a DashEmojiMart picker. Click the map to place the marker; a form popup opens above it with name/type fields. Pick an emoji at any time to update the marker's icon live. Create finalizes the marker (pushes to a markers store, exits create mode, closes everything); Cancel discards.

(We use dmc.Popover, not dmc.HoverCard. HoverCard is hover-triggered only — it would close the moment the user moves their mouse off the button toward the map to place a marker. Popover supports click-triggered + controlled opened, same visual styling.) """

import dash import dash_leaflet2 as dl2 import dash_mantine_components as dmc from dash import Input, Output, State, callback, clientside_callback, ctx, dcc, html from dash_emoji_mart import DashEmojiMart from dash_iconify import DashIconify from dl2_tiles import ESRI_CANVAS, register_theme_swap from dl2_locations import DENVER from dl2_shared import code_panel, header, info_panel

CARTO Positron (light) + Dark Matter — the standard light/dark pair the rest of the

showcase already uses (see assets/leaflet2_maps.js). The tile URL is swapped at

runtime via a clientside callback driven by the app's color-scheme toggle.

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 = ESRI_CANVAS TILE_URL = TILES.url("light") ATTR = TILES.attribution()

MARKER_TYPES = ["Market", "Event", "Bounty", "Other"] TYPE_COLORS = {"Market": "green", "Event": "blue", "Bounty": "grape", "Other": "gray"}

CODE = """dl2.Map(children=[ dl2.TileLayer(), dl2.EasyButton(id="add", icon="mdi:emoticon-happy-outline"), # placed by callback after map click in create mode: dl2.Marker(position=pending["lat,lng"], emoji=pending["emoji"], draggable=True, children=dl2.Popup(opened=True, closeOnClick=False, closeButton=False, children=form_layout(pending))) ])

Popover anchored to an invisible div positioned over the EasyButton:

dmc.Popover(opened=picker_open, position="right-start", children=[ dmc.PopoverTarget(html.Div(id="anchor")), # invisible, on top of the button dmc.PopoverDropdown(DashEmojiMart(id="emoji")), ])"""

---- helpers ---------------------------------------------------------------

def form_layout(pending): """The form rendered INSIDE the leaflet popup above the pending marker.""" return html.Div( style={"minWidth": "260px", "padding": "2px"}, children=dmc.Stack( [ dmc.TextInput( id="eb-name", placeholder="Marker name", value=(pending or {}).get("name", ""), size="xs", ), dmc.SegmentedControl( id="eb-type", data=MARKER_TYPES, value=(pending or {}).get("type", "Market"), size="xs", fullWidth=True, ), dmc.Group( [ dmc.Button( "Cancel", id="eb-cancel", size="xs", variant="light", color="gray", ), dmc.Button( "Create", id="eb-create", size="xs", color="green", leftSection=DashIconify(icon="mdi:check", width=14), ), ], gap="xs", grow=True, ), ], gap="xs", ), )

---- layout ----------------------------------------------------------------

component = dmc.Stack( [ header( "Easy Button — marker creation flow", "Click the smile button on the map → emoji picker opens to its right and you enter " "create mode. Click the map to drop a draggable marker; a form popup opens above it " "for name + type. Picking a new emoji updates the marker icon live. Create finalizes; " "Cancel discards.", badge="dl2.EasyButton", ), # Toggle button for the side panel. Mirrors /resize-observer's UX so this page # gets the same "expand for full-screen map / collapse to see context" affordance. dmc.Button( "Toggle side panel", id="eb-panel-toggle", color="green", variant="light", leftSection=DashIconify(icon="mdi:panel-right-open", width=16), ), # Flex row: relative-positioned map wrapper on the left, collapsible info panel on # the right. Starts OPEN (the Mode badge + marker list are useful context while # creating markers); the user can collapse it for a full-width map. We bump the # panel width via --dl2-resize-panel-w because the code snippet inside is wider # than what /resize-observer's default 320px shows comfortably. html.Div( id="eb-row", className="dl2-resize-row open", style={"height": "62vh", "--dl2-resize-panel-w": "380px"}, children=[ html.Div( # Relative container so the Popover anchor div positions absolutely over # the EasyButton location inside the Paper. dl2-resize-main makes this # the flex-grow main area (height: 100%, flex: 1 — see assets/style.css). className="dl2-resize-main", style={"position": "relative"}, children=[ dmc.Paper( dl2.Map( id="eb-map", center=DENVER.center, zoom=12, style={ "height": "100%" }, # fills the dmc.Paper, which fills the flex row children=[ dl2.TileLayer( id="eb-tile", url=TILE_URL, attribution=ATTR ), dl2.EasyButton( id="eb-add", position="topleft", icon="mdi:emoticon-happy-outline", iconSize=20, title="Add an emoji marker", ), html.Div(id="eb-pending-container"), html.Div(id="eb-markers-container"), ], ), shadow="sm", radius="md", withBorder=True, style={"overflow": "hidden", "height": "100%"}, ), # Popover anchor: invisible Dash element positioned over the EasyButton. # Leaflet anchors top-left controls 10px from the map edges, button is # 30px wide → anchor at left:46px puts the popover dropdown right of it. dmc.Popover( id="eb-popover", opened=False, position="right-start", offset=6, withArrow=True, arrowSize=10, shadow="lg", radius="md", # Fully suppress the "outside click closes me" behavior — we control # opened from the picker-open store. Listening to no events means # the popover never tries to auto-close. closeOnClickOutside=False, closeOnEscape=False, clickOutsideEvents=[], keepMounted=True, children=[ # PopoverTarget wraps its child in a dmc.Box that drops inline # absolute styles on the inner element — so position the Box # itself via boxWrapperProps instead. dmc.PopoverTarget( html.Div(id="eb-popover-anchor"), boxWrapperProps={ "style": { "position": "absolute", "top": "10px", "left": "46px", "width": "1px", "height": "30px", "pointerEvents": "none", "zIndex": 600, } }, ), dmc.PopoverDropdown( DashEmojiMart( id="eb-emoji", theme="auto", perLine=8, emojiSize=22, emojiButtonSize=30, previewPosition="none", ), p=4, ), ], ), ], ), # Side panel — holds everything that used to live in the right grid column # and the bottom code panel: Mode readout, Created markers list, and the # snippet showing the dl2.EasyButton + marker-creation pattern. html.Div( className="dl2-resize-panel", children=dmc.Stack( [ info_panel( "Mode", dmc.Group( [ dmc.Badge( id="eb-mode-badge", color="gray", variant="light", children="view", ), DashIconify( id="eb-mode-icon", icon="mdi:eye-outline", width=20, color="var(--mantine-color-dimmed)", ), ], gap="xs", ), ), info_panel( "Created markers", html.Div( id="eb-list", children=dmc.Text( "Click the smile button on the map to create your first marker.", size="sm", c="dimmed", ), ), ), code_panel( "dl2.EasyButton + marker-creation pattern", CODE ), ], gap="md", ), ), ], ), # State stores dcc.Store(id="eb-mode", data="view"), dcc.Store(id="eb-pending", data=None), dcc.Store(id="eb-markers", data=[]), dcc.Store(id="eb-picker-open", data=False), ], gap="md", )

---- callbacks --------------------------------------------------------------

1) EasyButton click → toggle create mode + picker.

@callback( Output("eb-mode", "data"), Output("eb-picker-open", "data"), Output("eb-pending", "data"), Input("eb-add", "n_clicks"), State("eb-mode", "data"), prevent_initial_call=True, ) def toggle_create(_, mode): if mode == "create": # Toggling off while in create mode: bail out without finalizing. return "view", False, None return "create", True, None

2) Picker open state → DMC Popover.opened

@callback(Output("eb-popover", "opened"), Input("eb-picker-open", "data")) def reflect_picker(opened): return bool(opened)

3) Mode badge readout.

@callback( Output("eb-mode-badge", "children"), Output("eb-mode-badge", "color"), Output("eb-mode-icon", "icon"), Output("eb-mode-icon", "color"), Input("eb-mode", "data"), ) def mode_readout(mode): if mode == "create": return "create", "green", "mdi:pencil-outline", "var(--mantine-color-green-6)" return "view", "gray", "mdi:eye-outline", "var(--mantine-color-dimmed)"

4) Picking an emoji updates pending (creating a stub if none yet) + resets the picker.

@callback( Output("eb-pending", "data", allow_duplicate=True), Output("eb-emoji", "value"), Input("eb-emoji", "value"), State("eb-mode", "data"), State("eb-pending", "data"), prevent_initial_call=True, ) def pick_emoji(emoji, mode, pending): if not emoji or mode != "create": return dash.no_update, dash.no_update base = pending or {} return {**base, "emoji": emoji}, ""

5) Map click while in create mode → place the pending marker (only first click).

@callback( Output("eb-pending", "data", allow_duplicate=True), Input("eb-map", "clickData"), State("eb-mode", "data"), State("eb-pending", "data"), prevent_initial_call=True, ) def place_pending(click, mode, pending): if mode != "create" or not click: return dash.no_update if pending and "lat" in pending: return dash.no_update # already placed; further clicks ignored (drag to move) lat, lng = click["latlng"] base = pending or {} return { **base, "lat": lat, "lng": lng, "name": base.get("name", ""), "type": base.get("type", "Market"), }

6) Cancel → discard pending + exit create mode.

@callback( Output("eb-mode", "data", allow_duplicate=True), Output("eb-pending", "data", allow_duplicate=True), Output("eb-picker-open", "data", allow_duplicate=True), Output("eb-emoji", "value", allow_duplicate=True), Input("eb-cancel", "n_clicks"), prevent_initial_call=True, ) def cancel(n): # Cancel/Create are dynamically mounted via render_pending; Dash fires their # callbacks on first mount with n_clicks=None despite prevent_initial_call=True. # Guard against the mount-fire so it doesn't immediately close the popover. if not n: return dash.no_update, dash.no_update, dash.no_update, dash.no_update return "view", None, False, ""

7) Create → finalize pending (push to markers) + exit.

@callback( Output("eb-markers", "data"), Output("eb-mode", "data", allow_duplicate=True), Output("eb-pending", "data", allow_duplicate=True), Output("eb-picker-open", "data", allow_duplicate=True), Output("eb-emoji", "value", allow_duplicate=True), Input("eb-create", "n_clicks"), State("eb-pending", "data"), State("eb-name", "value"), State("eb-type", "value"), State("eb-markers", "data"), prevent_initial_call=True, ) def create(n, pending, name, type_, markers): # Same mount-fire guard as cancel — without it, the Create button mounting fires # this callback with n=None and closes everything before the user can interact. if not n: return ( dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, ) if not pending or "lat" not in pending: return dash.no_update, "view", None, False, "" final = {**pending, "name": name or "", "type": type_ or "Market"} return list(markers or []) + [final], "view", None, False, ""

8) Render the pending marker (+ form popup) into the map.

@callback(Output("eb-pending-container", "children"), Input("eb-pending", "data")) def render_pending(pending): if not pending or "lat" not in pending: return [] emoji = pending.get("emoji") return dl2.Marker( id="eb-pending-marker", position=[pending["lat"], pending["lng"]], emoji=emoji, iconSize=34 if emoji else None, draggable=True, children=dl2.Popup( id="eb-pending-popup", opened=True, closeOnClick=False, autoClose=False, closeButton=False, maxWidth=320, minWidth=260, children=form_layout(pending), ), )

9) Drag the pending marker → update its position in pending.

@callback( Output("eb-pending", "data", allow_duplicate=True), Input("eb-pending-marker", "position"), State("eb-pending", "data"), prevent_initial_call=True, ) def drag_pending(pos, pending): if not pending or not pos: return dash.no_update if pos[0] == pending.get("lat") and pos[1] == pending.get("lng"): return dash.no_update return {**pending, "lat": pos[0], "lng": pos[1]}

10) Render finalized markers (view mode).

@callback( Output("eb-markers-container", "children"), Output("eb-list", "children"), Input("eb-markers", "data"), ) def render_markers(markers): markers = markers or [] if not markers: empty = dmc.Text( "Click the smile button on the map to create your first marker.", size="sm", c="dimmed", ) return [], empty children = [] for i, m in enumerate(markers): emoji = m.get("emoji") type_ = m.get("type", "Other") name = m.get("name") or "(unnamed)" children.append( dl2.Marker( position=[m["lat"], m["lng"]], emoji=emoji, iconSize=34 if emoji else None, children=[ dl2.Tooltip(children=name), dl2.Popup( children=html.Div( style={"minWidth": "180px"}, children=dmc.Stack( [ dmc.Text(name, fw=600, size="sm"), dmc.Badge( type_, color=TYPE_COLORS.get(type_, "gray"), variant="light", size="sm", ), ], gap=4, ), ) ), ], ) ) listing = dmc.Stack( [ dmc.Group( [ dmc.Text(m.get("emoji") or "📍", size="lg"), dmc.Stack( [ dmc.Text(m.get("name") or "(unnamed)", size="sm", fw=600), dmc.Badge( m.get("type", "Other"), color=TYPE_COLORS.get(m.get("type", "Other"), "gray"), variant="light", size="xs", ), ], gap=0, ), ], gap="sm", ) for m in markers ], gap="sm", ) return children, listing

---- side-panel toggle (mirrors /resize-observer) --------------------------

Toggle the .open class on the flex row; the panel slides in/out via CSS

transition (assets/style.css → .dl2-resize-row + .dl2-resize-panel). Because

the map's container is a flex item, Leaflet 2's ResizeObserver picks up the

width change and reflows the tiles automatically — no invalidateSize() needed.

clientside_callback( """ (n) => { const r = document.getElementById('eb-row'); if (r) r.classList.toggle('open'); return window.dash_clientside.no_update; } """, Output("eb-panel-toggle", "id"), Input("eb-panel-toggle", "n_clicks"), prevent_initial_call=True, )

---- theme sync (light/dark) ------------------------------------------------

The header toggle (color-scheme-toggle.checked) drives the app's color scheme —

checked=True means light. Mirror it to the dl2.TileLayer URL (CARTO Positron vs

Dark Matter) and to DashEmojiMart's theme prop so the picker UI follows along

too. Same pattern the /emoji-iconify page uses.

register_theme_swap("eb-tile", TILES)

clientside_callback( # Same fix as the tile swap: read the STORE, not the header ActionIcon. "(scheme) => (scheme === 'dark' ? 'dark' : 'light')", Output("eb-emoji", "theme"), Input("color-scheme-storage", "data"), ) ```

:defaultExpanded: false :withExpandedButton: true

---

*Source: /easy-button*

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: