Beta — the core lifecycle, permissions, composition, and testing utilities are stable, but the public API can still change before 1.0. Not yet published to PyPI (see Installation).
Server-driven UI components for Python web frameworks, in the style of Phoenix LiveView and Laravel Livewire: state and event handling live on the server, and HTMX handles the client-side wiring instead of a JavaScript framework.
Adding interactivity to a server-rendered Python app usually means one of two paths: hand-roll a pile of endpoint-specific JavaScript, or bring in a separate SPA framework (React, Vue) and split your app into a Python API plus a JS frontend. Both add real cost — a second toolchain and build pipeline for the SPA route, or a growing pile of bespoke fetch/DOM-patching code for the hand-rolled route.
Component Framework gives you a third option, closer to how Phoenix LiveView, Rails Hotwire, and Laravel Livewire work: your component's Python class owns state and event handlers, the server renders HTML (via Jinja2, JinjaX, or Django templates), and a single ~500-line vanilla JS client (no build step, no bundler) sends events and swaps the returned markup into the page. There's no client-side state store to keep in sync with the server.
It's aimed at teams already building on FastAPI, Django, Litestar, or Flask who want LiveView-style interactive widgets — dashboards, forms, admin panels, real-time counters/carts — without adding a second frontend stack.
Browser (HTMX + component-client.js)
| fetch POST /components/<name> (event + payload)
Framework Adapter (FastAPI / Django / Litestar / Flask)
|
Component Framework Core
- Lifecycle: mount → hydrate → handle_event → render → dehydrate
- Event routing: on_<event> methods, resolved by convention (sync + async)
- State: server-owned JSON, round-tripped to the client each request
- Renderer: pluggable (JinjaX, Django templates, Jinja2, or custom)
|
Backend (database / services)
Each request round-trip re-hydrates the component from the state the previous response sent down, dispatches the event to an on_<event> (or async def on_<event>) handler, re-renders, and returns HTML plus the new state. The client (component-client.js, in src/component_framework/static/) has no framework dependency — it reads data-component/data-event/data-payload attributes, POSTs the event, and reconciles the response into the DOM via Idiomorph, preserving focus, scroll position, in-flight input, and unaffected nodes' identity (including already-bound event listeners) instead of a full markup replace.
Because state round-trips through the client by default, the framework includes:
- State size guards — a 64 KB warning and a 512 KB hard limit on serialized state (
core/component.py). - Optional HMAC state signing — sign outbound state and reject tampered/unsigned state on the way back in (
docs/STATE_SIGNING.md). - Locked fields — declare state keys (roles, prices, user IDs) that the client can never influence; they're stripped from what's sent to the client and re-derived server-side on every request (
docs/LOCKED_FIELDS.md).
| Framework | HTTP dispatch | WebSocket | SSE streaming | Renderer | Install extra |
|---|---|---|---|---|---|
| FastAPI | Yes | Yes (fastapi_websocket.py) |
Yes | JinjaX | [fastapi] |
| Django | Yes (FBV + CBV) | Yes (Channels) | Yes | Django templates, django-cotton |
[django] |
| Litestar | Yes | Yes (litestar_websocket.py) |
Yes | Jinja2 | [litestar] |
| Flask | Yes | Not yet (planned) | Not yet (planned) | Flask's Jinja2 environment | [flask] |
A few things are Django-only today even though the underlying hook is framework-agnostic in core/:
- CSRF protection — enforced via Django's
CsrfViewMiddleware. FastAPI, Litestar, and Flask have no CSRF enforcement in the adapter itself; seedocs/SECURITY_CSRF.mdfor the full per-adapter audit and integration guidance before putting cookie/session auth in front of those adapters. RateLimitMixinandCacheMixin— both wrap Django's cache framework (adapters/django_ratelimit.py,adapters/django_views.py); no FastAPI/Litestar/Flask equivalent exists yet.- Server-confirmed optimistic patches —
Component.get_optimistic_patch()is defined on the framework-agnostic base class, but only the Django adapter currently surfaces it in the response payload. Client-side optimistic prediction (data-optimisticattributes incomponent-client.js) works with every adapter, since it's pure client-side JS talking to any of them over HTTP.
Not on PyPI yet — install from source:
git clone https://github.com/fsecada01/component-framework.git
cd component-framework
# uv (recommended)
uv pip install -e ".[fastapi]" # or [django] / [litestar] / [flask] / [all]
# or with pip
pip install -e ".[fastapi]"pydantic>=2.0 is the only mandatory dependency; everything else — FastAPI, Django, Litestar, Flask, JinjaX, Channels — is an optional extra so you only pull in what you use.
pip install -e ".[fastapi]" # single adapter
pip install -e ".[fastapi,django,litestar,flask]" # several
pip install -e ".[all]" # everything, including dev-adjacent websockets extraExtras were made optional in 0.3.0 — if you're on an older checkout that assumed
fastapi/uvicorn/jinjaxinstalled by default, see CHANGELOG.md.
python examples/fastapi_example.py
# http://localhost:8000from component_framework.core import Component, registry
@registry.register("counter")
class Counter(Component):
template_name = "counter.html"
def mount(self):
self.state["count"] = 0
def on_increment(self, amount: int = 1):
self.state["count"] += amountWiring an existing JinjaX Catalog (most FastAPI + JinjaX apps already have one, often sharing its Jinja environment with Jinja2Templates) — pass that catalog in rather than creating a fresh one, or component templates silently lose your app's filters/globals/extensions:
from component_framework.adapters.jinjax_renderer import JinjaxRenderer
from component_framework.core.component import Component
Component.renderer = JinjaxRenderer(catalog) # the catalog your app already builtcd examples/django_example
python manage.py migrate
python manage.py runserver
# http://localhost:8000python examples/litestar_example.py
# http://localhost:8000python examples/flask_example.py
# http://localhost:5000from flask import Flask
from component_framework.adapters.flask import FlaskRenderer, register_component_routes
from component_framework.core.component import Component
app = Flask(__name__)
Component.renderer = FlaskRenderer(app) # shares app.jinja_env
register_component_routes(app) # POST /components/<name>Core (framework-agnostic, core/)
- Component lifecycle with
mount/hydrate/handle_event/render/dehydrate - Async event handlers —
async def on_*, dispatched viaasync_dispatch() - Pluggable renderers — JinjaX, Django templates, Jinja2, or a custom
Renderer FormComponent— Pydantic-validated forms with field-level errors, state kept in syncSlotComponent/CompositeComponent— named slots and composing components from named children, with context propagationStreamingComponent— SSE endpoints for long-running operations with intermediate renders (FastAPI, Litestar, Django)- Permission classes —
AllowAny,IsAuthenticated,IsStaff,IsSuperuser,DjangoModelPermission; checked automatically wherever a component declarespermission_classes ComponentTestCase— mount components and dispatch events in tests without a running server;dispatch_event(),mount_component(),assert_state(),assert_rendered()
Security
- Optional HMAC-SHA256 state signing (
core/signing.py,StateSigner) — versionedcfs1.<payload>.<mac>tokens, key rotation via comma-separated keys, tampered/raw-dict state rejected with HTTP 400 locked_fields— server-trusted state keys stripped from outbound state and re-derived on every inbound request- 64 KB warn / 512 KB hard limit on serialized state size
- CSRF: enforced on Django only today — see
docs/SECURITY_CSRF.md
Django-specific
- Model binding (
DjangoModelComponent) withselect_related/prefetch_relatedand transactional saves - Class-based views with auth/permission handling and JSON (not redirect) error responses
django-cottontemplate support- Django Channels WebSocket consumer with broadcast/subscribe
RateLimitMixin(sliding-window, per-component/per-user, JSON 429s) andCacheMixin(render caching via Django's cache framework)
# Pydantic-validated form
from pydantic import BaseModel, EmailStr
from component_framework.core import FormComponent
class ContactSchema(BaseModel):
name: str
email: EmailStr
message: str
@registry.register("contact_form")
class ContactForm(FormComponent):
schema = ContactSchema
def on_submit(self):
send_email(self.validated_data)# Composition: slots + a composite parent
from component_framework.core.composition import SlotComponent, CompositeComponent
@registry.register("card")
class Card(SlotComponent):
template_name = "card.html"
slots = ["header", "body", "footer"]
@registry.register("product_page")
class ProductPage(CompositeComponent):
components = {"card": Card, "cart": CartComponent}# Django model-bound component
from component_framework.adapters.django_model import DjangoModelComponent
@registry.register("order_editor")
class OrderEditor(DjangoModelComponent):
model = Order
state_fields = ["status", "notes", "total"]
select_related = ["customer"]
def on_update_status(self, status: str):
self.instance.status = status
self.save_instance()# Testing a component without an HTTP server
from component_framework.testing import ComponentTestCase
class TestCounter(ComponentTestCase):
def test_increment(self):
component = self.mount_component("counter")
self.assert_state(component, count=0)
self.dispatch_event(component, "increment", amount=5)
self.assert_state(component, count=5)More worked examples: docs/examples/ecommerce.md (real-time cart), docs/examples/wizard.md (multi-step FastAPI wizard), and the runnable apps under examples/.
Full docs (generated from docstrings via pdoc, versioned per release): fsecada01.github.io/component-framework
| Guide | Covers |
|---|---|
| Architecture spec | Core design goals and component lifecycle |
| Django implementation | Django adapter setup and patterns |
| Class-based views | Django CBV auth/permission patterns |
| State signing | HMAC state setup per adapter, key rotation |
| Locked fields | Server-trusted state fields, threat model |
| CSRF & CSWSH guide | Per-adapter CSRF audit, WebSocket hijacking guidance |
| Client-side DOM morphing | Idiomorph integration, data-no-morph escape hatch for JS-owned regions |
| E-commerce example | Real-time cart + product walkthrough |
| Multi-step wizard | FastAPI wizard recipe |
component-framework/
├── src/component_framework/
│ ├── core/ # Framework-agnostic: component, form, state, streaming,
│ │ # registry, renderer, permissions, composition, signing
│ ├── adapters/ # fastapi[.py|_websocket.py], litestar[.py|_websocket.py],
│ │ # flask.py, django_*.py, jinjax_renderer.py
│ ├── testing.py # ComponentTestCase + pytest fixtures
│ ├── static/ # component-client.js (vanilla JS, no build step) + CSS
│ └── templatetags/ # Django template tags
├── examples/ # fastapi_example.py, fastapi_form_example.py,
│ # fastapi_wizard_example.py, litestar_example.py,
│ # flask_example.py, django_example/ (full app)
├── tests/ # 28 modules, 477 tests (pytest -q)
├── docs/ # Guides (Markdown) + pdoc-generated API site
└── pyproject.toml
pytest tests/ -q --tb=short
# or via the justfile
just test # full suite
just test-core # core/ only
just test-adapters # adapters/ onlyCI (.github/workflows/ci.yml) runs the suite on Python 3.11, 3.12, 3.13, and 3.14, plus ruff and ty (Astral's type checker), on every push and pull request.
just install # install with the dev extra (uv pip install -e ".[dev]")
just pre-commit-install # ruff + ty pre-commit hooks
just format / just lint / just lint-fix / just check
just docs-build / just docs-serve # pdoc, http://localhost:8000Contributions: open an issue for anything non-trivial before starting; small fixes can go straight to a PR. See CONTRIBUTING.md.
- CSRF is enforced on Django only; FastAPI, Litestar, and Flask have no CSRF protection in the adapter — read
docs/SECURITY_CSRF.mdbefore putting cookie/session auth in front of them. - No origin check or handshake token on any WebSocket adapter (Cross-Site WebSocket Hijacking exposure) — see the same doc.
RateLimitMixinandCacheMixinare Django-only.- Flask has no WebSocket or SSE support yet.
- Component state must be JSON-serializable, capped at 512 KB serialized.
- WebSocket fan-out across multiple server processes requires a Redis-backed channel layer (Django Channels).
Beta features through 0.6.0 (permissions, rate limiting, caching, composition, testing utilities, the Litestar and Flask adapters, optional HMAC state signing, locked fields, Idiomorph-based DOM morphing with focus/scroll/in-flight-input preservation, stable list reconciliation keys, a JS-owned-region escape hatch, and a CSRF/CSWSH coverage audit) are shipped. What's next, in order, is scoped in CHANGELOG.md and tracked via GitHub milestones:
- 0.7.0b — table stakes: SPA-style navigation (history, back button), file uploads, on-blur partial validation, a verified 422 re-render convention across adapters, WebSocket reconnection/resync, Flask WebSocket/SSE parity.
- 0.8.0b — mindshare: telemetry/observability hooks, published benchmarks (none exist yet — no performance numbers are claimed anywhere else in this README), a JS interop/optimistic-command DSL.
- 1.0.0: frozen public API, a full narrative guide, a deployment guide, PyPI publishing.
- Python 3.11+
pydantic>=2.0(only mandatory runtime dependency)
Optional extras: [fastapi] (FastAPI 0.109+, Uvicorn, JinjaX 0.41+), [django] (Django 4.2+, Channels 4.0+, channels-redis 4.1+, django-cotton 0.9+), [litestar] (Litestar 2.0+, Jinja2 3.1+), [flask] (Flask 3.0+), [websockets] (websockets 12.0+), [all].
MIT — see LICENSE.
Inspired by Phoenix LiveView, Laravel Livewire, Hotwire/Turbo, and HTMX.