Skip to main content

Component Framework

Beta — the core lifecycle, permissions, composition, and testing utilities are stable, but the public API can still change before 1.0. 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.

CI Docs Python 3.11+ License: MIT


The problem this solves

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.


How it works

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 support

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; see docs/SECURITY_CSRF.md for the full per-adapter audit and integration guidance before putting cookie/session auth in front of those adapters.
  • RateLimitMixin and CacheMixin — both wrap Django's cache framework (adapters/django_ratelimit.py, adapters/django_views.py); no FastAPI/Litestar/Flask equivalent exists yet.
  • Server-confirmed optimistic patchesComponent.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-optimistic attributes in component-client.js) works with every adapter, since it's pure client-side JS talking to any of them over HTTP.

Installation

# uv (recommended)
uv add "component-framework[fastapi]"     # or [django] / [litestar] / [flask]

# or with pip
pip install "component-framework[fastapi]"

Pick the extra for the web framework you're on. pydantic>=2.0 is the only mandatory dependency; everything else — FastAPI, Django, Litestar, Flask, JinjaX, Channels — is optional, so you only pull in what you use.

pip install "component-framework[fastapi]"                       # single adapter
pip install "component-framework[fastapi,django,litestar,flask]"  # several
pip install "component-framework[all]"                            # every adapter, plus the websockets extra
pip install "component-framework[fastapi,testing]"                # + the pytest helpers, see Testing below

The quotes matter in most shells — bare brackets are glob syntax in zsh and get eaten before pip sees them.

Import an adapter whose extra you skipped and you get a deliberate error naming the fix, not a bare ModuleNotFoundError:

ImportError: 'jinjax' is not installed. Install the 'fastapi' extra:
pip install 'component-framework[fastapi]'

Extras were made optional in 0.3.0 — if you're upgrading from before that and assumed fastapi/uvicorn/jinjax came by default, see CHANGELOG.md.

From a checkout

For hacking on the framework itself:

git clone https://github.com/fsecada01/component-framework.git
cd component-framework
uv pip install -e ".[dev]"

See CONTRIBUTING.md.


Quick start

FastAPI

python examples/fastapi_example.py
# http://localhost:8000
from 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"] += amount

Wiring 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 built

Django

cd examples/django_example
python manage.py migrate
python manage.py runserver
# http://localhost:8000

Litestar

python examples/litestar_example.py
# http://localhost:8000

Flask

python examples/flask_example.py
# http://localhost:5000
from 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>

Features

Core (framework-agnostic, core/)

  • Component lifecycle with mount / hydrate / handle_event / render / dehydrate
  • Async event handlers — async def on_*, dispatched via async_dispatch()
  • Pluggable renderers — JinjaX, Django templates, Jinja2, or a custom Renderer
  • FormComponent — Pydantic-validated forms with field-level errors, state kept in sync
  • SlotComponent / CompositeComponent — named slots and composing components from named children, with context propagation
  • StreamingComponent — SSE endpoints for long-running operations with intermediate renders (FastAPI, Litestar, Django)
  • Permission classes — AllowAny, IsAuthenticated, IsStaff, IsSuperuser, DjangoModelPermission; checked automatically wherever a component declares permission_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) — versioned cfs1.<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) with select_related/prefetch_related and transactional saves
  • Class-based views with auth/permission handling and JSON (not redirect) error responses
  • django-cotton template support
  • Django Channels WebSocket consumer with broadcast/subscribe
  • RateLimitMixin (sliding-window, per-component/per-user, JSON 429s) and CacheMixin (render caching via Django's cache framework)

More examples

# 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: a parent declares named slots, children fill them
from component_framework.core import Component, registry
from component_framework.core.composition import compose

@registry.register("card")
class Card(Component):
    template_name = "card.html"
    slots = ["header", "body", "footer"]   # omit to accept any slot name

@registry.register("cart_summary")
class CartSummary(Component):
    template_name = "cart_summary.html"

# Assemble in one call. Each child's rendered HTML lands in the parent's
# template context under `slots`, keyed by slot name.
page = compose(
    Card,
    params={"title": "Your order"},
    body=CartSummary(),
)
result = page.dispatch()
# 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.
# Needs the `testing` extra: pip install "component-framework[testing]"
from component_framework.testing import ComponentTestCase

class TestCounter(ComponentTestCase):
    component_class = Counter          # a MockRenderer is installed per test

    def test_initial_state(self):
        result = self.mount()
        assert result["state"]["count"] == 0

    def test_increment(self):
        self.mount()
        self.dispatch("increment", {"amount": 5})
        self.assert_state(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/.


Documentation

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

Project layout

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

Testing

pytest tests/ -q --tb=short

# or via the justfile
just test              # full suite
just test-core         # core/ only
just test-adapters     # adapters/ only

CI (.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.


Development

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:8000

Contributions: open an issue for anything non-trivial before starting; small fixes can go straight to a PR. See CONTRIBUTING.md.


Known limitations

  • CSRF is enforced on Django only; FastAPI, Litestar, and Flask have no CSRF protection in the adapter — read docs/SECURITY_CSRF.md before 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.
  • RateLimitMixin and CacheMixin are 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).

Roadmap

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.

Requirements

  • 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].


License

MIT — see LICENSE.

Inspired by Phoenix LiveView, Laravel Livewire, Hotwire/Turbo, and HTMX.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

component_framework-0.6.0.tar.gz (349.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

component_framework-0.6.0-py3-none-any.whl (86.7 kB view details)

Uploaded Python 3

File details

Details for the file component_framework-0.6.0.tar.gz.

File metadata

  • Download URL: component_framework-0.6.0.tar.gz
  • Upload date:
  • Size: 349.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for component_framework-0.6.0.tar.gz
Algorithm Hash digest
SHA256 bfa5fecd602e31a60e2daaba88dffaac6e75f0d338817d64cac18359672dad68
MD5 4c9e7f478dc673218d44f5277aed79ff
BLAKE2b-256 03ddfbec0a8d7585bfd419410e584eb951d0c6491c98257b6636e218342903ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for component_framework-0.6.0.tar.gz:

Publisher: release.yml on fsecada01/component-framework

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file component_framework-0.6.0-py3-none-any.whl.

File metadata

File hashes

Hashes for component_framework-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d7470fbaed84e17d2f0d6a5440e7fcb407f67f8bd5d5872770c122a1487d7fa5
MD5 cccd2333764ccd28cd16a38688f4a34b
BLAKE2b-256 f6f19355bb7322afe6cac392da4e1893025a317773409f8fc658e5f45fdc079d

See more details on using hashes here.

Provenance

The following attestation bundles were made for component_framework-0.6.0-py3-none-any.whl:

Publisher: release.yml on fsecada01/component-framework

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page