Read and write the Excalidraw shape library from Python with lastLibraryChange and updateLibrary.

Library

Read and write the Excalidraw shape library from Python with lastLibraryChange and updateLibrary.


Overview

The library menu holds reusable shapes. lastLibraryChange fires whenever the user adds/removes an item; command: updateLibrary lets you push items in from Python.

Coming back from the public library

The sidebar's Browse Library button sends the user to Excalidraw's public library site. libraryReturnUrl is the URL that site sends them back to once they pick a shape:

DashExcalidraw(
    id="canvas",
    libraryReturnUrl="https://your-app.example.com/drawing",
)

It has no default of its own. Left unset, the return trip is whatever Excalidraw decides, which is not a URL your app chose — so set it to the page hosting the canvas whenever the library button is reachable, and the user comes back to you rather than wherever the fallback points.

Live demo

Source

# File: docs/library/library.py

"""Library: observe library items and drive updateLibrary."""

import uuid

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

from dash_excalidraw import DashExcalidraw
from docs._shared import (
    canvas_frame,
    code_block,
    json_panel,
    sync_canvas_theme,
    two_column,
)

sync_canvas_theme("library-canvas")

CODE = """
# observe
@callback(Output('out', 'children'),
          Input('canvas', 'lastLibraryChange'))
def show(snapshot): ...

# drive (merge items from Python)
@callback(Output('canvas', 'command'),
          Input('btn', 'n_clicks'),
          prevent_initial_call=True)
def add_items(_):
    return {'id': str(uuid.uuid4()),
            'type': 'updateLibrary',
            'payload': {
                'libraryItems': [...],
                'merge': True,
                'openLibraryMenu': True,
            }}
"""


component = dmc.Stack(
    gap="md",
    children=[
        code_block(CODE),
        two_column(
            canvas_frame(
                DashExcalidraw(
                    id="library-canvas",
                    height="560px",
                ),
                min_height=560,
            ),
            dmc.Stack(
                gap="sm",
                children=[
                    dmc.Button(
                        "Open library menu",
                        id="library-open-btn",
                        variant="light",
                    ),
                    dmc.Paper(id="library-panel", withBorder=True, p="xs"),
                ],
            ),
        ),
    ],
)


@callback(
    Output("library-panel", "children"),
    Input("library-canvas", "lastLibraryChange"),
)
def _render_library(snapshot):
    if not snapshot:
        return dmc.Text("(no library changes yet)", c="dimmed", size="sm")
    items = snapshot.get("items") or []
    return dmc.Stack(
        gap="xs",
        children=[
            dmc.Group(
                [
                    dmc.Text("Library items", size="sm", fw=600),
                    dmc.Badge(len(items), variant="light"),
                ]
            ),
            json_panel("Raw payload", snapshot, height=320),
        ],
    )


@callback(
    Output("library-canvas", "command"),
    Input("library-open-btn", "n_clicks"),
    prevent_initial_call=True,
)
def _open_library(_clicks):
    return {
        "id": f"libopen-{uuid.uuid4()}",
        "type": "toggleSidebar",
        # `name` is the SIDEBAR, `tab` is the panel inside it. Excalidraw's
        # built-in sidebar is called "default" and "library" is one of its
        # tabs — so `{"name": "library"}` addresses a sidebar that does not
        # exist, and `toggleSidebar` answers False and does nothing at all.
        # That is what made this button look dead while the canvas's own
        # library button opened the very same panel.
        "payload": {"name": "default", "tab": "library", "force": True},
    }

:defaultExpanded: false :withExpandedButton: true


Source: /library

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: