value vs selectedEmoji, reacting to outside clicks, and persisting a selection across reloads.

Callbacks & Props

value vs selectedEmoji, reacting to outside clicks, and persisting a selection across reloads.


Two ways to read a selection

Every pick writes both props in one update, so a callback with both as Inputs fires once, not twice.

PropTypeContents
valuestremoji.native for a built-in emoji ("😀"), or emoji.src — the image URL — for a custom one.
selectedEmojidictThe whole emoji-mart object.

value is the 0.0.x contract and is unchanged, so existing callbacks keep working. selectedEmoji was added in 0.2.0 for callbacks that need more than the glyph:

{
    "id": "grinning",
    "name": "Grinning Face",
    "native": "😀",
    "unified": "1f600",
    "keywords": ["face", "smile", "happy", "joy", "grin"],
    "shortcodes": ":grinning:",
    "skin": 1,
    "aliases": ["grinning_face"],
}

A custom or Iconify emoji carries src instead of native — which is the cleanest way to tell the two apart:

@callback(Output("out", "children"), Input("picker", "selectedEmoji"))
def show(emoji):
    if not emoji:
        return "Nothing picked"
    if emoji.get("src"):
        return html.Img(src=emoji["src"], style={"height": 32})
    return emoji["native"]

Live demo

Reacting to a click outside

emoji-mart hands its onClickOutside handler a DOM event, which cannot cross into Python. The component surfaces it as clickedOutside, a counter with the same shape as n_clicks — it increments on each outside click:

@callback(
    Output("popover", "opened", allow_duplicate=True),
    Input("picker", "clickedOutside"),
    prevent_initial_call=True,
)
def close(_n):
    return False

Only the change matters; the number itself is meaningless. Pair it with prevent_initial_call=True so a fresh mount does not fire the callback.

dmc.Popover already dismisses itself on an outside click and writes opened=False back to Dash, so clickedOutside earns its keep on panels you built yourself — a dmc.Collapse, a styled html.Div, an absolutely-positioned card. See Picker in a Popover for what the popover case does and does not need.

Clicking an emoji, the search box or a category tab does not increment it, and neither does the click that opened the picker.

Both are worth stating because emoji-mart's own onClickOutside does the opposite on both counts: it fires whenever the event target is not exactly the picker's root element — so every emoji click counts as "outside" — and it registers its listener during mount, while the opening click is still bubbling, so it reports an outside click before the user has clicked anything. Wired to a popover, that closed the popover on the same click that opened it. clickedOutside is measured against the component's wrapper and bound one task after mount, so neither happens.

Persistence

value is persisted by default once persistence is switched on, using Dash's standard persistence machinery:

DashEmojiMart(
    id="picker",
    persistence=True,
    persisted_props=["value", "selectedEmoji"],
    persistence_type="local",   # "local" | "session" | "memory"
)

persisted_props defaults to ["value"] and persistence_type to "local".

emoji-mart keeps its own frequently-used tally in localStorage under its own key, independently of Dash persistence. It survives a reload whether or not persistence is set; maxFrequentRows=0 is what turns it off.

Pattern-matching ids

id accepts a dict, so pickers work inside ALL/MATCH callbacks like any other component:

DashEmojiMart(id={"type": "emoji-picker", "index": row_id})

@callback(
    Output({"type": "emoji-out", "index": MATCH}, "children"),
    Input({"type": "emoji-picker", "index": MATCH}, "value"),
)
def show(value):
    return value

Props that need a remount

Most props update in place. Three are read only when emoji-mart builds its internal store, so changing them on a mounted picker has no visible effect:

PropWhy
setSelects the spritesheet, loaded at init.
localeBuilds the translated search index at init.
customMerged into the emoji store at init.

Force a remount by varying the id of a wrapper:

return html.Div(DashEmojiMart(id="picker", set=chosen_set), id=f"mount-{chosen_set}")

Dash's renderer keys each child on its id, so a new wrapper id is a new React key and the subtree is thrown away rather than patched. A key= prop does not work for this — it is accepted by html components but never read during reconciliation, and it surfaces in the console as ` key is not a prop `.

emoji-mart's store is module-global, not per-component, and categories is filtered against a snapshot taken on the first initialisation in the page's lifetime. A remount runs a later initialisation, which is exactly the case where custom ids no longer resolve — so remounting does not rescue the combination, it triggers the failure. Omit categories when you pass custom. See Custom emojis.

Source

# File: docs/callbacks/example.py

"""`value`, `selectedEmoji` and `clickedOutside` shown side by side, live.

Both selection props are Inputs to one callback on purpose: the component writes them in a
single `setProps`, so this fires once per pick rather than twice.
"""

import json

import dash_mantine_components as dmc
from dash import Input, Output, callback, html

from dash_emoji_mart import DashEmojiMart

component = dmc.Group(
    [
        DashEmojiMart(
            id="cb-picker",
            perLine=8,
            emojiSize=22,
            maxFrequentRows=1,
            # Persist the pick across reloads. Reload the page after choosing
            # one — the readout comes back with it.
            persistence=True,
            persisted_props=["value", "selectedEmoji"],
            persistence_type="local",
        ),
        dmc.Stack(
            [
                dmc.Paper(
                    [
                        dmc.Group(
                            [
                                dmc.Text("value", size="sm", fw=600),
                                dmc.Badge("str", variant="light", size="sm"),
                            ],
                            gap="xs",
                        ),
                        dmc.Code(id="cb-value", children="None"),
                    ],
                    withBorder=True,
                    p="md",
                    radius="md",
                ),
                dmc.Paper(
                    [
                        dmc.Group(
                            [
                                dmc.Text("selectedEmoji", size="sm", fw=600),
                                dmc.Badge("dict", variant="light", size="sm"),
                            ],
                            gap="xs",
                        ),
                        dmc.CodeHighlight(
                            id="cb-object", code="None", language="json"
                        ),
                    ],
                    withBorder=True,
                    p="md",
                    radius="md",
                ),
                dmc.Paper(
                    [
                        dmc.Group(
                            [
                                dmc.Text("clickedOutside", size="sm", fw=600),
                                dmc.Badge("int", variant="light", size="sm"),
                            ],
                            gap="xs",
                        ),
                        dmc.Text(
                            id="cb-outside",
                            children="0 — click anywhere off the picker",
                            size="sm",
                            c="dimmed",
                        ),
                    ],
                    withBorder=True,
                    p="md",
                    radius="md",
                ),
            ],
            gap="md",
            style={"flex": 1, "minWidth": 320},
        ),
    ],
    align="flex-start",
    gap="xl",
)


@callback(
    Output("cb-value", "children"),
    Output("cb-object", "code"),
    Input("cb-picker", "value"),
    Input("cb-picker", "selectedEmoji"),
)
def show(value, emoji_obj):
    if not value:
        return "None", "None"
    return repr(value), json.dumps(emoji_obj, indent=2, ensure_ascii=False)


@callback(
    Output("cb-outside", "children"),
    Input("cb-picker", "clickedOutside"),
    prevent_initial_call=True,
)
def outside(n):
    return f"{n} — the counter increments, the number itself is not meaningful"

Source: /callbacks

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: