Slim, server-side, component-driven framework on top of Plotly Dash.
Project description
OOP for Plotly Dash — make your dashboards composable.
Vanilla Dash and Dash Pages give you primitives, not encapsulation: callbacks live globally, IDs are strings you manage by hand, and the same widget can't be dropped into two apps without surgery. Weaverlet packages layout + callbacks + state into reusable Python classes, with auto-unique IDs, typed inter-component signals, and a keep_mounted router that preserves WebGL state across navigation.
Weaverlet, developed by the Observatorio Metropolitano CentroGeo, turns a Plotly Dash app into a hierarchy of reusable Python classes. Each WeaverletComponent owns its own layout, callbacks, and identifiers; you compose them into a tree, hand the root to WeaverletApp, and ship.
It's aimed at:
- Data scientists whose dashboard has outgrown a single file and needs structure.
- Dash developers who want true component reuse instead of copy-paste between apps.
- React-style developers looking for the same class-based component paradigm, server-side, in pure Python.
What Weaverlet adds on top of vanilla Dash
- Class-based encapsulation — layout, callbacks, state, and IDs live together in one class. Instantiate it twice for two independent copies that don't step on each other.
- Auto-unique IDs — the
Identifier()descriptor generates a Dash-unique ID per instance. No more managing string IDs by hand or hittingDuplicateIdErrorwhen reusing a widget. - Signal-based events —
SignalComponent+SignalInput/SignalOutput/SignalTriggerreplace ad-hocdcc.Store+ manual callback wiring as the canonical inter-component event bus. - State-preserving routing (new in 0.3) —
SimpleRouterComponent(keep_mounted=True)keeps every route mounted and toggles CSS, so WebGL canvases (dash-leaflet maps, dash-sylvereye graphs, Plotly WebGL) and React state (n_clicks, selections, scroll position) survive navigation. - Session-based auth routing —
AuthRouterComponentgates routes viaflask.sessionout of the box. - Shared context — one dict propagates to every component in the DAG; no prop-drilling, no globals.
Weaverlet vs Dash Pages
Dash Pages (built into Dash 2.5+) and Weaverlet both solve "multi-page Dash" but at different levels of abstraction. Pages is a routing convention — drop a file in pages/, register it, get a route. Weaverlet is a component framework — write classes, compose them, navigate without losing state.
| Capability | Dash Pages | Weaverlet |
|---|---|---|
| Built-in to Dash | ✓ (Dash ≥ 2.5) | One pip install |
| Routing source | Filesystem (pages/*.py) |
Programmatic routes dict, computed at runtime |
Path templating (/users/<id>) |
✓ | Via pathname / search parsing |
| Page-level encapsulation | One module per page | Reusable component classes; same class can power N routes |
| Cross-page widget reuse | Imports + manual callback wiring | Instantiate the same class anywhere; auto-unique IDs |
| Inter-component events | dcc.Store + global callbacks |
SignalComponent typed events |
| State across navigation | Unmount + remount on every route change | keep_mounted=True preserves React + WebGL state |
| Auth gating | DIY | AuthRouterComponent + Flask sessions |
| Auto-discovered nav menu | ✓ (dash.page_registry) |
Build it yourself |
Reach for Dash Pages when your dashboard is content-heavy, each page is roughly independent, you want filesystem-driven routing, and you don't need to preserve state across navigation. It's the simpler tool and ships with Dash.
Reach for Weaverlet when you have shared widget logic across pages, when WebGL components (maps, network graphs, 3D plots) shouldn't reset on navigation, when you want typed inter-component events instead of dcc.Store plumbing, or when your dashboard has grown past the "one file per page" model and needs proper encapsulation.
The two aren't mutually exclusive — Weaverlet apps can coexist alongside a Pages-based site in the same Dash deployment if that fits your migration path.
Weaverlet vs Dash All-in-One Components
All-in-One Components (AIO, built into Dash 2+) and Weaverlet share a goal — bundle a Dash widget's layout and callbacks into one unit — but they operate at different scopes. AIO is a widget pattern: subclass html.Div, register pattern-matching callbacks once at module level, give each instance a UUID aio_id. Weaverlet is a whole-app framework: a tree of components with lifecycle hooks, routing, signals, and shared context.
| Capability | Dash AIO | Weaverlet |
|---|---|---|
| Built-in to Dash | ✓ (Dash ≥ 2.0) | One pip install |
| ID style | Pattern-matching dicts ({'component': ..., 'aio_id': uuid}) |
Flat strings via Identifier() descriptor |
| Callback registration | Once at module level via MATCH / ALL |
Per instance, in register_callbacks(self, app) |
Instance state on self |
Awkward (callbacks are module-level free functions) | Native (callbacks close over self) |
| Composing hierarchies | Limited — each AIO is typically a leaf | DAG of nested components; parent/child wiring is automatic |
| Multipage routing | Not provided (pair with Dash Pages or DIY) | SimpleRouterComponent + AuthRouterComponent |
| Inter-component events | Pattern-matching callbacks or DIY dcc.Store |
Typed SignalComponent events |
| State across navigation | Up to surrounding router (none by default) | keep_mounted=True preserves React + WebGL state |
| Shared context / config | DIY | Built-in dict propagated to every component |
| Auth gating | DIY | AuthRouterComponent + Flask sessions |
Reach for AIO when you're building a single reusable widget — a fancy date picker, a search box, a chart-with-controls — to drop into existing Dash apps, especially when you want many instances and need to operate on all of them at once via pattern-matching. It's the lighter pattern for one widget.
Reach for Weaverlet when you're structuring a full application — multipage, auth, signal flows, shared context, state-preserving navigation. It's the framework for the whole dashboard.
They coexist nicely. Weaverlet components are regular Dash components under the hood, so you can use AIO widgets as leaves inside a WeaverletComponent.get_layout() without friction. The reverse (Weaverlet inside an AIO) is technically possible but usually awkward — Weaverlet wants to own the lifecycle of its own tree.
What's new in 0.3.0
Released April 2026 — full notes in CHANGELOG.md.
- Dash 4.x support; Python ≥ 3.12; dependency pins relaxed (Flask / werkzeug now resolved transitively, no longer hard-pinned).
SimpleRouterComponent(keep_mounted=True, preserve_path="/...")for state preservation across route changes.- Top-level imports —
from weaverlet import WeaverletComponent, ...works directly; no need to drill intoweaverlet.base/weaverlet.components. - Permissive
get_layoutsignatures — components declare any subset of(pathname, hash, href, search, user, protected_route)and the router passes only what's declared. DashProxyunder the hood sodash_extensions.javascript.assign(),Serverside(...)return wrappers, andgroup=multi-output callbacks all work without extra wiring.assets_folderauto-resolves to the user's main script directory — no manual asset path setup needed.- 60-test pytest suite; build via
pyproject.toml+ hatchling. jupyter-dashmoved to an optional extra (pip install weaverlet[jupyter]).
Install
pip install weaverlet
(or uv add weaverlet if you're on uv.)
Optional extras:
| Extra | Pulls in | Use when |
|---|---|---|
weaverlet[examples] |
dash-bootstrap-components, dash-mantine-components |
Running the bundled examples 11–14 |
weaverlet[jupyter] |
dash[jupyter] |
Constructing WeaverletApp(jupyter_mode=True) |
weaverlet[dev] |
pytest, pytest-playwright |
Hacking on Weaverlet itself |
Quick start
A single-page app that echoes user input:
from weaverlet import WeaverletComponent, WeaverletApp, Identifier
from dash_extensions.enrich import Input, Output
from dash import html, dcc
class EchoComponent(WeaverletComponent):
text_input_id = Identifier()
echo_div_id = Identifier()
def get_layout(self):
return html.Div([
dcc.Input(id=self.text_input_id, type="text"),
html.Div(id=self.echo_div_id),
])
def register_callbacks(self, app):
@app.callback(Output(self.echo_div_id, "children"),
Input(self.text_input_id, "value"))
def echo(text_value):
return text_value or ""
WeaverletApp(root_component=EchoComponent()).app.run()
A multipage app with WebGL-safe state preservation:
from weaverlet import (
WeaverletComponent, WeaverletApp, SimpleRouterComponent,
)
from dash import html
class HomePage(WeaverletComponent):
def get_layout(self):
return html.Div("Home — your dash-leaflet map goes here.")
class AboutPage(WeaverletComponent):
def get_layout(self):
return html.Div("About — switch back to Home; the map's zoom survives.")
class NotFound(WeaverletComponent):
def get_layout(self):
return html.Div("404")
router = SimpleRouterComponent(
routes={"/": HomePage(), "/about": AboutPage()},
not_found_page_component=NotFound(),
keep_mounted=True, # keep all routes mounted; toggle CSS only
preserve_path="/", # which route owns the document flow (default: first)
)
WeaverletApp(root_component=router).app.run()
Aliasing — multiple paths pointing at the same component instance — works in both modes. Under
keep_mounted=True, aliased paths automatically share a single wrapper, so the sameIdentifier-bearing layout never mounts twice.
Examples
The examples/ folder has 14 self-contained scripts, ordered by complexity:
| # | File | Demonstrates |
|---|---|---|
| 01 | 01_helloworld_app.py |
Minimal layout-only component |
| 02 | 02_echo_app.py |
Layout + callbacks + Identifier |
| 03 | 03_greeting_app.py |
Constructor-injected state |
| 04 | 04_router_app.py |
Multipage with SimpleRouterComponent |
| 05 | 05_auth_router_app.py |
Session-protected routes via AuthRouterComponent |
| 06 | 06_redirect_app.py |
Programmatic redirect from a callback |
| 07 | 07_signal_input.py |
Signals carrying a payload |
| 08 | 08_signal_trigger.py |
Edge-only triggers |
| 09 | 09_signal_chain_app.py |
Chained signal pipeline |
| 10 | 10_div_signal_trigger_app.py |
Signal-driven <div> updates |
| 11 | 11_dbc_app.py |
Single-page Weaverlet + Dash Bootstrap |
| 12 | 12_dbc_multipage_app.py |
Multipage with DBC navbar |
| 13 | 13_dbc_modal_app.py |
DBC modal triggered by signals |
| 14 | 14_dmc_multipage_app.py |
Dash Mantine + AppShell + keep_mounted state preservation |
Compatibility
| Python | ≥ 3.12 |
| Dash | ≥ 4.1.0, < 5.0.0 |
| dash-extensions | ≥ 1.0.0, < 3.0.0 (1.x and 2.x both supported) |
For LLM coding assistants
Weaverlet ships a ReadMe.LLM.md following the ReadMe.LLM methodology — rules, context, 14 worked examples, API signatures, and patterns specifically formatted for LLMs like Claude, GPT, and Codex. To use it from another project, drop one of these URLs into your prompt or have your assistant fetch it:
- Latest:
https://cdn.jsdelivr.net/gh/observatoriogeo/weaverlet@main/ReadMe.LLM.md - Pinned to v0.3.0:
https://cdn.jsdelivr.net/gh/observatoriogeo/weaverlet@v0.3.0/ReadMe.LLM.md
There's also an llms.txt at the repo root following the llmstxt.org convention — a short index that tools like Cursor and Claude Code can auto-discover.
Contributing & support
- Bug reports and feature requests: GitHub issues.
- Tests:
pip install -e ".[dev]"thenpytest(60 tests, ~1 second). - Migrating from 0.2.x? See CHANGELOG.md — most code keeps working unchanged; the main gotcha is
app.run_server()→app.run()(Dash 4 removed the old name).
License
Weaverlet is open-source software licensed under the MIT license.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file weaverlet-0.3.0.tar.gz.
File metadata
- Download URL: weaverlet-0.3.0.tar.gz
- Upload date:
- Size: 20.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ba3a9cd192d1737ea6cf3091fcee5dc7e27c1558c99a6f48543d987c80ed59f
|
|
| MD5 |
9e0eda1c01eba1b1a4c6f70cff65dc6f
|
|
| BLAKE2b-256 |
27c64cce98f62b5bf6a7323f7bb45a81a19877623d6b5517345060e5f3f0ecd0
|
File details
Details for the file weaverlet-0.3.0-py3-none-any.whl.
File metadata
- Download URL: weaverlet-0.3.0-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
69b0e10b4432dfaa55cd41d1776c637d9529dbabc81ec5f89348761a054745f6
|
|
| MD5 |
d91441bb6e56e422cc85b3cdca95a67e
|
|
| BLAKE2b-256 |
24842d0dd5788b770c4d71cc04b90e3eb018413c26defb565983433ccb1e0260
|