**Site index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.ai/llms.txt](https://2plot.ai/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 11 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://2plot.dev/sitemap.xml  




Features
The Excalidraw editor (pip dash package) supports:

- 💯 Free & open-source.
- 🎨 Infinite, canvas-based whiteboard.
- ✍️ Hand-drawn like style.
- 🌓 Dark mode.
- 🏗️ Customizable.
- 📷 Image support.
- 😀 Shape libraries support.
- 👅 Localization (i18n) support.
- 🖼️ Export to PNG, SVG & clipboard.
- 💾 Open format - export drawings as an .excalidraw json file.
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
- ➡️ Arrow-binding & labeled arrows.
- 🔙 Undo / Redo.
- 🔍 Zoom and panning support.
- 🚀 +Many more...

### Installation
[Visit GitHub Repo](https://github.com/pip-install-python/dash_excalidraw)
```bash
pip install dash-excalidraw
```

### What's New in 0.1.0

Version 0.1.0 is a full rebuild on Excalidraw `0.18.x` with a **JSON-safe prop surface** — every prop round-trips cleanly through Dash callbacks. There are no function props, no React children, no RegExp values: just JSON.

Props now fall into three groups:

| Kind | Direction | Props |
|:-----|:----------|:------|
| **Declarative** | Python → canvas | `theme`, `viewModeEnabled`, `zenModeEnabled`, `gridModeEnabled`, `isCollaborating`, `UIOptions`, `validateEmbeddable`, `interceptLinkOpens`, `hideExcalidrawLinks`, `langCode`, `name` |
| **Event snapshots** (read-only) | canvas → Python | `elements`, `appState`, `files`, `serializedData`, `externalizedSerializedData`, `sceneVersion`, `lastPointerDown`, `lastPointerUp`, `lastPointerMove`, `lastScrollChange`, `lastPaste`, `lastLibraryChange`, `lastLinkOpen`, `lastExport`, `lastFileAdded`, `lastExternalDrop` |
| **Command dispatch** | Python → canvas (imperative) | `command` — write `{id, type, payload}` from a callback |

#### Migrating from 0.0.x

| Old (0.0.x) | New (0.1.0) |
|:------------|:------------|
| `onPointerUpdate`, `onPointerDown`, `onScrollChange`, `onPaste`, `onLibraryChange`, `onLinkOpen` | `lastPointerMove`, `lastPointerDown`, `lastScrollChange`, `lastPaste`, `lastLibraryChange`, `lastLinkOpen` |
| `excalidrawAPI` callback | `command` prop + `lastExport` event |
| `renderTopRightUI`, `renderCustomStats`, `renderEmbeddable`, `generateIdForFile` | Removed — function props cannot round-trip through JSON |
| `isCollaborating` defaulted `True` | Defaults `False` — opt in explicitly |
| `gridModeEnabled` defaulted `True` | Defaults `False` |
| `height` defaulted `"400px"` | Defaults `"600px"` |
| `appState` output: `{gridSize, viewBackgroundColor}` only | Full serializable appState |
| `validateEmbeddable: RegExp[]` | `validateEmbeddable: list[str]` domain globs (compiled to RegExps internally) |

### Introduction



```python
# File: docs/dash_excalidraw/introduction.py

import dash_excalidraw
from dash import Dash, html, dcc, callback, Input, Output, State
import json
import dash_mantine_components as dmc
from dash_ace import DashAceEditor

# _dash_renderer._set_react_version("18.2.0")

class CustomJSONDecoder(json.JSONDecoder):
    def decode(self, s):
        result = super().decode(s)
        return self._decode(result)

    def _decode(self, o):
        if isinstance(o, bool):
            return o
        if isinstance(o, dict):
            return {k: self._decode(v) for k, v in o.items()}
        if isinstance(o, list):
            return [self._decode(v) for v in o]
        if o == "true":
            return True
        if o == "false":
            return False
        if o == "null":
            return None
        return o


def custom_pprint(obj, indent=2):
    def format_value(v):
        if isinstance(v, (dict, list)):
            return custom_pprint(v, indent)
        elif v is True:
            return 'True'
        elif v is False:
            return 'False'
        elif v is None:
            return 'None'
        else:
            return repr(v)

    if isinstance(obj, dict):
        items = [f"{' ' * indent}{repr(k)}: {format_value(v)}" for k, v in obj.items()]
        return "{\n" + ",\n".join(items) + "\n}"
    elif isinstance(obj, list):
        items = [f"{' ' * indent}{format_value(v)}" for v in obj]
        return "[\n" + ",\n".join(items) + "\n]"
    else:
        return repr(obj)


initialCanvasData = {}

component = html.Div([
dmc.Tabs(
    [
        dmc.TabsList(
            [
                dmc.TabsTab(
                    "Dash Excalidraw",
                    # leftSection=DashIconify(icon="tabler:message"),
                    value="dashecalidraw-component",
                    style={'font-size': '1.5rem', 'color': 'light-dark(rgb(28, 126, 214), rgb(116, 192, 252))'}
                ),
                dmc.TabsTab(
                    "DashExcalidraw .json Output",
                    # leftSection=DashIconify(icon="tabler:settings"),
                    value="canvas-output",
                    style={'font-size': '1.5rem', 'color': 'light-dark(rgb(28, 126, 214), rgb(116, 192, 252))'}
                ),
            ]
        ),
        dmc.TabsPanel(dash_excalidraw.DashExcalidraw(
        id='excalidraw',
        width='100%',
        height='65vh',
        initialData=initialCanvasData,
        # validateEmbeddable=False,
        # isCollaborating=False,
    ), value="dashecalidraw-component"),
        dmc.TabsPanel(html.Div([
            html.Div(id='number-of-elements'),
            html.Div(id='output')
        ]), value="canvas-output"),
    ],
    value="dashecalidraw-component",
),
    # dcc.Interval(id='interval', interval=1000)
]
)


@callback(
    Output('output', 'children'),
    Output('number-of-elements', 'children'),
    Input('excalidraw', 'serializedData'),
)
def display_output(serializedData):
    if not serializedData:
        return 'No elements drawn yet', 'Number of elements: 0'

    # Parse the serialized data with custom decoder
    data = json.loads(serializedData, cls=CustomJSONDecoder)

    # Count the number of elements
    num_elements = len(data.get('elements', []))

    # Use custom pretty-print function
    output = custom_pprint(data, indent=2)

    # Add a key to force re-rendering
    return DashAceEditor(
        id='dash-ace-editor',
        value=f'{output}',
        theme='monokai',
        mode='python',
        tabSize=2,
        enableBasicAutocompletion=True,
        enableLiveAutocompletion=True,
        autocompleter='/autocompleter?prefix=',
        placeholder='Python code ...',
        style={'height': '500px', 'width': '80vw'}
    ),  html.Label(f"Number of elements: {num_elements}")




```


### Simple Example



```python
# File: docs/dash_excalidraw/simple_example.py

from dash_excalidraw import DashExcalidraw
from dash import Dash, html, dcc, callback, Input, Output, State

initialCanvasData = {}

component = html.Div([
        DashExcalidraw(
        id='excalidraw-simple',
        width='100%',
        height='80vh',
        initialData=initialCanvasData,
    )
    ])
```


### Commands and the Export Round-Trip

Anything that used to require the removed `excalidrawAPI` callback is now an imperative **command**. Write a dict to the `command` prop from a Python callback:

```python
@callback(Output("board", "command"), Input("update-btn", "n_clicks"),
          prevent_initial_call=True)
def send_command(_):
    return {
        "id": f"cmd-{uuid.uuid4()}",   # MUST be unique — drives de-duplication
        "type": "updateScene",          # see supported types below
        "payload": {...},               # shape depends on type
    }
```

Supported `type` values:

| Scene mutation | Async export (round-trips via `lastExport`) | Other |
|:---------------|:--------------------------------------------|:------|
| `updateScene` | `exportToSvg` | `setActiveTool` |
| `resetScene` | `exportToBlob` | `setToast` |
| `addFiles` | `exportToCanvas` | `toggleSidebar` |
| `replaceFiles` | | `updateLibrary` |
| `scrollToContent` | | |

Each dispatch is de-duplicated by `id`, and the component clears the prop once the action completes so React re-renders do not re-fire it.

Exports are **async**: dispatch the export command in one callback, then observe the result on the `lastExport` prop (`{timestamp, id, type, result, error?}`) in a *separate* callback. Always match on `id` or `type` — `lastExport` holds the result of *some* export, not necessarily your latest command.

Draw something below, then click **Export to SVG**:



```python
# File: docs/dash_excalidraw/export_roundtrip.py

"""Export round-trip example for dash-excalidraw 0.1.0.

Demonstrates the imperative `command` prop and the `lastExport` event:
a Python callback dispatches an `exportToSvg` (or `setActiveTool`)
command, and a second callback observes the async result on `lastExport`.
"""
import uuid

import dash_mantine_components as dmc
from dash import Input, Output, callback, ctx, html, no_update
from dash_excalidraw import DashExcalidraw

component = html.Div(
    [
        DashExcalidraw(
            id="excalidraw-export-canvas",
            width="100%",
            height="50vh",
            initialData={
                "elements": [],
                "appState": {},
                "scrollToContent": True,
            },
        ),
        dmc.Group(
            [
                dmc.Button(
                    "Export to SVG",
                    id="excalidraw-export-svg-btn",
                    color="blue",
                ),
                dmc.Button(
                    "Select Rectangle Tool",
                    id="excalidraw-export-tool-btn",
                    variant="light",
                    color="teal",
                ),
            ],
            mt="sm",
            mb="sm",
        ),
        html.Div(id="excalidraw-export-preview"),
    ]
)


@callback(
    Output("excalidraw-export-canvas", "command"),
    Input("excalidraw-export-svg-btn", "n_clicks"),
    Input("excalidraw-export-tool-btn", "n_clicks"),
    prevent_initial_call=True,
)
def dispatch_command(_svg_clicks, _tool_clicks):
    """Write a {id, type, payload} dict to `command` to act on the canvas.

    The `id` must be unique per dispatch — the component de-duplicates
    on it, then clears the prop once the action completes.
    """
    if ctx.triggered_id == "excalidraw-export-tool-btn":
        return {
            "id": f"tool-{uuid.uuid4()}",
            "type": "setActiveTool",
            "payload": {"type": "rectangle"},
        }
    return {
        "id": f"export-svg-{uuid.uuid4()}",
        "type": "exportToSvg",
        "payload": {"exportPadding": 20},
    }


@callback(
    Output("excalidraw-export-preview", "children"),
    Input("excalidraw-export-canvas", "lastExport"),
    prevent_initial_call=True,
)
def render_export(result):
    """Exports are async — observe them in a separate callback.

    Always match on `id`/`type`: `lastExport` is the result of *some*
    export, not necessarily the latest command you dispatched.
    """
    if not result or result.get("type") != "exportToSvg":
        return no_update
    if result.get("error"):
        return dmc.Alert(str(result["error"]), color="red", title="Export failed")
    return dmc.Paper(
        [
            dmc.Text("SVG export result:", size="sm", c="dimmed", mb="xs"),
            html.Iframe(
                srcDoc=result.get("result", ""),
                style={
                    "width": "100%",
                    "height": "300px",
                    "border": "none",
                    "background": "white",
                },
            ),
        ],
        withBorder=True,
        p="sm",
    )
```


### Initial Data

`initialData` seeds the scene on mount with `{elements, appState, files, libraryItems, scrollToContent}`. It is **mount-only** — updating the prop after render does nothing. To change the scene after mount, dispatch a `command` with `type="updateScene"`:

```python
@callback(
    Output("canvas", "command"),
    Input("restore-btn", "n_clicks"),
    State("store", "data"),
    prevent_initial_call=True,
)
def restore(_, snapshot):
    parsed = json.loads(snapshot)
    return {
        "id": f"restore-{uuid.uuid4()}",
        "type": "updateScene",
        "payload": {
            "elements": parsed.get("elements", []),
            "appState": parsed.get("appState", {}),
        },
    }
```

For persistence, stream `serializedData` into a `dcc.Store` — but guard against Excalidraw's mount-time empty scene (skip envelopes with no elements) or you'll clobber a valid snapshot on every page refresh. When the scene contains images, prefer persisting `externalizedSerializedData`, which strips inline base64 `data:` URIs.

### Theme and View Modes

`theme` (`"light"` / `"dark"`), `viewModeEnabled`, `zenModeEnabled`, and `gridModeEnabled` are plain declarative props — set them from any callback and the canvas reflects the change:

```python
# Follow your app's color scheme
clientside_callback(
    "function(scheme) { return scheme || 'light'; }",
    Output("canvas", "theme"),
    Input("color-scheme-store", "data"),
)
```

- `viewModeEnabled=True` — read-only canvas: drawing tools disabled, pan/zoom still available.
- `zenModeEnabled=True` — hides most of the chrome for a distraction-free canvas.
- `gridModeEnabled=True` — snap-to-grid plus grid background (now defaults to `False`).

### Component Properties

| Property | Type | Default | Description |
|:---------|:-----|:--------|:------------|
| id | string | - | Unique ID to identify this component in Dash callbacks |
| width | string | `'100%'` | CSS width of the canvas container |
| height | string | `'600px'` | CSS height of the canvas container (Excalidraw fills its parent) |
| initialData | dict | - | Initial scene on mount: `{elements, appState, files, libraryItems, scrollToContent}`. Mount-only — use `command: updateScene` afterwards |
| command | dict | - | Imperative dispatch: `{id, type, payload}`. De-duplicated by unique `id`; cleared after the action completes |
| theme | `'light'` / `'dark'` | `'light'` | Canvas color theme |
| viewModeEnabled | boolean | `False` | View-only mode: disables drawing tools; pan/zoom still available |
| zenModeEnabled | boolean | `False` | Zen mode hides most of the chrome |
| gridModeEnabled | boolean | `False` | Snap to grid and draw the grid background |
| isCollaborating | boolean | `False` | Renders the collaborator UI; feed `appState.collaborators` yourself (no transport bundled) |
| UIOptions | dict | - | JSON-serializable subset of Excalidraw `UIOptions`: `canvasActions`, `tools.image`, `welcomeScreen`, `dockedSidebarBreakpoint` |
| validateEmbeddable | boolean or list of strings | - | `True` allow all, `False` deny all, or domain globs (e.g. `["*.youtube.com"]`) compiled to RegExps internally |
| interceptLinkOpens | boolean | `False` | Prevent default on link opens so Python can handle `lastLinkOpen` itself |
| hideExcalidrawLinks | boolean | `True` | Hides Excalidraw's built-in GitHub/Discord/Twitter menu group |
| langCode | string | `'en'` | UI language code (e.g. `en`, `fr-FR`, `zh-CN`) |
| name | string | - | Drawing name — appears in the top bar and export filenames |
| autoFocus | boolean | `True` | Focus the canvas on mount |
| detectScroll | boolean | `True` | Whether Excalidraw listens to wheel-scroll events on the canvas |
| handleKeyboardGlobally | boolean | `True` | Keyboard shortcuts work even when the canvas is not focused |
| libraryReturnUrl | string | - | Optional URL appended to the "Browse Library" button |
| pointerMoveThrottleMs | number | `50` | Debounce interval for `lastPointerMove` writes (ms) |
| scrollThrottleMs | number | `100` | Debounce interval for `lastScrollChange` writes (ms) |
| elements | list of dicts | - | Current element array (read-only from Python) |
| appState | dict | - | Full serializable app state (read-only from Python) |
| files | dict | - | Binary file entries: image id → `{dataURL, mimeType, ...}` (read-only) |
| serializedData | string | - | JSON string of the canonical Excalidraw envelope `{type, version, source, elements, appState, files}` |
| externalizedSerializedData | string | - | Same envelope with inline `data:` URIs stripped to `null` — persist this to avoid base64 bloat |
| sceneVersion | number | - | Monotonic scene version — cheap change detection without diffing elements |
| lastExport | dict | - | Result of the most recent export command: `{timestamp, id, type, result, error?}` |
| lastPointerDown | dict | - | `{timestamp, activeTool, pointer: {x, y}}` |
| lastPointerUp | dict | - | `{timestamp, activeTool, pointer: {x, y}}` |
| lastPointerMove | dict | - | Throttled `{timestamp, pointer, button, pointersMap}` |
| lastScrollChange | dict | - | Throttled `{timestamp, scrollX, scrollY}` |
| lastPaste | dict | - | `{timestamp, data}` snapshot of the last clipboard paste |
| lastLibraryChange | dict | - | `{timestamp, items}` snapshot of the last library change |
| lastLinkOpen | dict | - | `{timestamp, elementId, url}` — fired on Cmd/Ctrl-click of a linked element |
| lastFileAdded | dict | - | Fires when new files appear with inline `data:` dataURLs; batch under `files`, first file at top level |
| lastExternalDrop | dict | - | Fires on drops Excalidraw doesn't accept (non-image or multi-file): `{timestamp, files, dropPoint, placeholderIds}` |

### Helpers

The package also exports file-handling helpers for the image-externalization workflow:

```python
from dash_excalidraw import decode_data_url, strip_inline_files, restore_inline_files
```

- `decode_data_url(data_url)` — split a `data:` URI into `(mime, raw_bytes)` for uploading.
- `strip_inline_files(serialized)` — remove inline base64 from a serialized envelope.
- `restore_inline_files(serialized, files)` — rehydrate inline bytes into a stripped envelope.
