Lively UI¶
Reactive Python UI Without JavaScript
Duck now includes the Lively Component System—a real-time, component-based system that enables responsive page updates without full reloads.
It leverages WebSockets with msgpack for fast communication and supports navigation to new URLs without full page reloads.
Recommended: Use Lively/HTML components over traditional templates. Components are flexible, Pythonic, and allow pluggable, reusable UI elements.
What Is the Lively Component System?¶
The Lively Component System is Duck’s way of building interactive web pages using pure Python.
Instead of writing JavaScript for buttons, forms, or live updates, you write Python classes. Duck automatically handles the real-time communication between the browser and the server for you.
Think of it like this:
You write Python.
Duck makes the browser react instantly.
What Problem Does It Solve?¶
Normally, web apps need:
HTML for structure
CSS for styling
JavaScript for interactivity
A backend language for logic
With Lively Components, you can:
Define UI elements in Python
Attach event handlers in Python
Update the page dynamically
Avoid writing JavaScript for most interactions
The system keeps the page updated without full reloads.
How It Works (Simple Explanation)¶
You create a Page class in Python.
You add components (like buttons, text, forms).
You attach Python functions to events (like button clicks).
When a user interacts with the page:
The browser sends the event to the server.
Your Python function runs.
Only the changed parts of the page update.
This feels similar to React or other reactive frameworks — but controlled from Python.
Why It’s Powerful¶
Real-time updates without manual JavaScript
Cleaner architecture (UI + logic in one place)
Fast navigation between pages
Component reuse
Built-in lifecycle handling
Beginner Mental Model¶
If you’re new, think of it like this:
A Page = a screen
A Component = a piece of UI (button, text, input)
An Event = something the user does (click, type)
Your Python method = what should happen next
And Duck handles the browser synchronization automatically.
Page Component Example¶
from duck.html.components.page import Page
from duck.html.components.button import Button
class HomePage(Page):
def on_create(self):
super().on_create()
self.set_title("Home - MySite")
self.set_description("Welcome to MySite, the premier platform...")
self.set_favicon("/static/favicon.ico", icon_type="image/x-icon")
self.set_opengraph(
title="Home - MySite",
description="Welcome to MySite, the premier platform...",
url="https://mysite.com",
image="https://mysite.com/og-image.png",
type="website",
site_name="MySite"
)
self.set_twitter_card(card="summary_large_image", title="Home - MySite")
self.set_json_ld({
"@context": "https://schema.org",
"@type": "WebSite",
"url": "https://mysite.com",
"name": "MySite",
"description": "Welcome to MySite, the premier platform..."
})
self.add_to_body(Button(text="Hello world"))
Organizing pages this way isolates page-specific logic in dedicated classes, making code easier to maintain, extend, and debug.
Guidelines¶
When vibecoding or working with these components, refer to the guidelines directory in our GitHub repository: https://github.com/duckframework/duck/tree/main/ai
This directory provides best practices for building scalable, maintainable components and structuring projects effectively.
Component Events¶
Lively components allow binding Python handlers to events like button clicks—no JS needed.
from duck.shortcuts import to_response
from duck.html.components.button import Button
from duck.html.components.page import Page
from duck.html.core.websocket import LivelyWebSocketView
from duck.html.core.exceptions import JSExecutionError, JSExecutionTimedOut
async def on_click(btn: Button, event: str, value: Any, websocket: LivelyWebSocketView):
"""
Button onclick event.
Args:
btn (Button): Button component.
event (str): Event name.
value (str): Current button value.
websocket (LivelyWebSocketView): Active WebSocket.
"""
btn.bg_color = "red" if btn.bg_color != "red" else "green"
try:
# Execute some JS directly
await websocket.execute_js('alert(`Javascript execution success`);')
except (JSExecutionTimedOut, JSExecutionError):
pass
def home(request):
page = Page(request)
# Create and add button to page
btn = Button(id="some-id", text="Hello world", bg_color="green", color="white")
page.add_to_body(btn)
# Bind an event to Python handler
btn.bind("click", on_click)
return to_response(page) # Or just return page if you don't want control over response.
Use
update_targetswhen multiple components must update on an event to avoid redundant updates.
Document-specific events¶
These are events bound directly to the document rather than HTML elements. Duck provides a way to bind to these events
but it’s only available on Page component instances.
Here is an example:
# views.py
from duck.contrib.sync import ensure_async
from duck.html.components.page import Page
from web.services.some_module import fetch_db_items
def home(request):
page = Page(request)
def on_navigation(page, event, path, websocket):
# Called whenever the browser navigates to a page.
print(f"Navigated to page {page}")
def on_back_navigation(page, event, path, websocket):
# Called when the user returns to a page using browser history
# (for example, the Back button).
print(f"Back navigated to page {page}")
async def on_page_load(page, event, _, websocket):
# Called only when a page is loaded for the first time.
# Not called when returning to a page via browser history.
print(f"Page loaded: {page}")
# Fetch data that should only be loaded on the initial page load.
items = await ensure_async(fetch_db_items)()
# Listen for document navigation events.
page.document_bind("DuckNavigated", on_navigation, update_self=False)
page.document_bind("DuckBackNavigated", on_back_navigation, update_self=False)
# Listen for the initial page load event.
page.document_bind("DOMContentLoaded", on_page_load, update_self=False)
# Return page.
return page
Automatic Event Binding¶
You can now bind events directly when creating components using the event_handlers argument:
Button(
event_handlers=[
{
"click": self.on_click,
"update_self": False,
"update_targets": [self],
**extra_kwargs,
}
]
)
For document-level events such as DOMContentLoaded, use document_event_handlers on Page:
Page(
document_event_handlers=[
{
"DOMContentLoaded": self.on_loaded,
**extra_kwargs,
}
]
)
Both event_handlers and document_event_handlers accept additional keyword arguments that are forwarded to their respective bind methods.
WebSocket sync convention¶
Any async method that pushes state to the client over an active WebSocket connection uses a ws_ prefix (e.g. ws_sync, ws_open, ws_dismiss). Plain methods (set_, show_, or any other state mutator) only update local component state and rely on Duck’s end-of-handler diffing to sync automatically — no ws_ prefix, no async.
Use ws_ methods only when the UI must update before the handler returns — e.g. before a slow operation, or to trigger a raw JS class toggle that Duck’s diffing can’t reach. Otherwise, prefer plain state mutators and let diffing handle the sync.
# Prefer this when possible — no ws needed.
component.show_error("Something went wrong")
# Use ws_ only when you need immediate feedback.
component.show_info("Uploading...")
await component.ws_open(ws)
Pre-rendering Components¶
Pre-rendering caches component outputs for faster loading.
Example:
from duck.html.components.page import Page
from duck.shortcuts import to_response
def home(request):
page = Page(request=request)
background_thread.submit_task(
lambda: page.pre_render(deep_traversal=True, reverse_traversal=True)
)
return to_response(page)
Counter App¶
Include the built-in counter blueprint to test:
BLUEPRINTS = [
"duck.etc.blueprints.counterapp.blueprint.CounterApp",
]
Visit /counterapp after adding it.
Notes¶
Component responses maximize the benefits of Lively Component System.
Fast navigation works best with component responses in views.
Components in Templates¶
Components can be used in Jinja2 and Django templates.
Jinja2 Example:
{{ Button(
id="btn",
text="Hello world",
)
}}
Django Example:
{% Button %}
id="btn",
text="Hello world",
{% endButton %}
Only add components to
TEMPLATE_HTML_COMPONENTSin settings.py for template usage.
Custom Components¶
from duck.html.components import InnerComponent
from duck.html.components.button import Button
from duck.shortcuts import to_response
class MyComponent(InnerComponent):
def get_element(self):
return "div"
def on_create(self):
super().on_create()
self.add_child(Button(text="Hi there"))
def home(request):
comp = MyComponent(request=request)
return to_response(comp)
Component Extensions¶
Extensions enhance component functionality.
from duck.html.components.button import Button
from duck.html.components.extensions import Extension
class MyExtension(Extension):
def apply_extension(self):
super().apply_extension()
self.style["background-color"] = "red"
class MyButton(MyExtension, Button):
pass
btn = MyButton() # Button with background-color "red"
Notes:
By default, all Lively components comes up with 2 builtin component extensions as follows:
BasicExtension: This is the basic extension which enables setting attributes like
color,bg_color& more to actually alter the componentstyle.StyleCompatibilityExtension: This enables compatibility of style properties like setting
backdrop-filterwill also set the-webkit-backdrop-filterand other compatibility style properties.
Predefined Components¶
All predefined components are available in duck.html.components and in default SETTINGS['TEMPLATE_HTML_COMPONENTS'].
Use these components to rapidly build responsive, interactive UIs in Duck.
Force Updates on Lively Components¶
It is possible to modify values set with Javascript with the use of Force updates or using ws.update_now method.
The following example will showcase this technique:
from duck.html.components import ForceUpdate
from duck.html.components.button import Button
from duck.html.components.script import Script
def on_btn_click(btn, *_):
# Return a force update to reset btn text to the initial text set on btn
# First argument to ForceUpdate is the component and second is list of updates.
# List of updates are "text"/"inner_html", "props", "style" and "all".
return ForceUpdate(btn, ["text"])
def home(request):
# Add a javascript callback to click event.
# This always gets called first before Duck event callback.'
btn = Button(text="Click me", id="btn", props={"onclick": "btnClick()"})
# Add some Javascript
script = Script(
inner_html="""
function btnClick() {
const btn = document.getElementId(`btn`);
btn.textContent = "Clicking..";
}
"""
)
# Add script to button
btn.add_child(script)
# Bind click event to python callable.
btn.bind("click", on_btn_click, update_self=False)
# Return btn, will be converted to response by Duck.
return btn
Critical rule: Lively performs non-destructive syncing.
It only updates props and styles it is aware of from the server state, and ignores anything it didn’t create or track.
New props/styles set from Python (e.g. in event handlers) will sync normally
Props/styles added externally (e.g. via
execute_js) are not trackedUntracked properties will not be removed or overridden, even when patches are applied
This means Lively will merge updates, not replace the entire prop/style object.
# Initial render — no background-color
btn = Button(text="Click")
# Later in Python (event handler) — this WILL sync
btn.style.update({"background-color": "red"})
// Added externally — Lively does NOT track this
element.style.border = "1px solid blue"
Even after future updates from Python:
btn.style.update({"background-color": "green"})
The border style will remain untouched because Lively never tracked it.
Key takeaway¶
Lively behaves like a partial diff system, not a full state replacement system:
✅ Updates known fields
✅ Adds new fields from server-side changes
❌ Does not remove unknown/external fields
Note
In cases where a property’s presence resolves to true regardless of its value e.g. The presence
of disabled or true, you can just execute JavaScript (using execute_js) to alter the component directly.
### Immediate Syncing
```{note}
Added in version 1.1.0
Method update_now synchronizes the current component state with the client immediately, without waiting for the dispatch loop’s final VDOM diff. Unlike deferred updates, this applies changes right away and can be safely called within a component event handler — useful when the client needs to reflect an intermediate state before the handler continues, restores state, or performs longer-running work.
Note
This method internally performs a ForceUpdate, sending the specified updates unconditionally — even if the resulting state is identical to what the client already has. Use this when you know a change occurred and want to guarantee it reaches the client without paying for a diff.
Example:
# Immediately sync state before continuing execution
async def on_click(btn, _, __, ws):
btn.text = "Clicking..."
await ws.update_now(btn, updates=["text"])
# Continue processing after UI reflects the change
btn = Button(text="Click me")
btn.bind("click", on_click, update_self=True)
Diffed Immediate Syncing¶
Note
Added in version 2.3.0
Method sync_now also synchronizes state with the client immediately from within an event handler, but unlike update_now, it diffs the component against its own last checkpoint first and sends only the patches that actually changed — no patch is sent for a no-op update. It is scoped to the target component’s own subtree, so syncing a deeply nested descendant (e.g. a button inside a page) never re-renders or re-diffs its ancestors.
Note
sync_now requires the target component to be a descendant of one of the event’s update_targets (or an update_target itself). Calling it on an unrelated component raises a ForceUpdateError.
Example:
# Diff and sync only if the state actually changed
async def on_click(btn, _, __, ws):
btn.text = "Clicking..."
await ws.sync_now(btn)
# Continue processing after UI reflects the change, with no
# patch sent at all if btn.text ends up unchanged by handler's end
btn = Button(text="Click me")
btn.bind("click", on_click, update_self=True)
sync_now vs update_now:
|
|
|
|---|---|---|
Sends a patch |
Always, unconditionally |
Only if state actually changed |
Cost |
Skips diffing — cheaper per call if you’re certain state changed |
Diffs first — avoids wasted network writes on no-ops |
Scope |
Whatever component/updates you pass |
Automatically scoped to the target’s own subtree |
Best for |
Small, definitely-changed components where a diff isn’t worth the CPU |
Any case with risk of a no-op, or when targeting a nested descendant without touching its ancestors |
---
## Handling Forms
**Duck** has got an easy mechanism you can use to easily handle form data.
### Example:
```py
from duck.html.components.form import Form
from duck.html.components.input import Input
from duck.html.components.fileinput import FileInput
def on_form_submit(form, event, value: dict, _):
name = value.get("name")
email = value.get("email")
file_metadata: dict = value.get("file") # Only file metadata will be resolved.
# Do something with the form data, maybe validation
def home(request):
form = Form(
fields=[
Input(type="text", name="name", placeholder="Name"),
Input(type="email", name="email", placeholder="email"),
FileInput(name="file"),
],
)
# Bind an event to form submission
form.bind("submit", on_form_submit)
# Return form or any parent
return form
From the above example, whenever submit event is bound to a component,
the JS method preventDefault() is called to avoid page reload.
Also, only file metadata (size, type & name) is received on submit event because Lively is not designed
to handle file uploads. You need to manually call internal api’s or views for file uploads and update the UI
if complete. One case you can do this is that you may execute JS for doing an AJAX request and update
the UI once the response is received.
File Uploads in Lively¶
Lively supports requesting file uploads from the client directly inside event handlers
via ws_request_file. When called, it waits for the user to select a file on the target
input and uploads it automatically once selected, with progress and status reported back
in real time.
Basic usage:
from duck.html.components.core.lively_utils.file_request import ws_request_file
from duck.http.fileuploads import FileVerificationError, FileTypeNotAllowedError
async def on_form_submit(..., ws):
file = await ws_request_file(form_id="...", name="...", ws)
# Verify file contents - only if allowed_mimes not passed to ws_request_file.
try:
file.verify() # You can pass allowed_mimes here as well
except (FileVerificationError, FileTypeNotAllowedError):
# Do something here
raise
# Do whatever you want with the file here - maybe saving
await file.async_save()
Advanced usage:
from duck.html.components.core.lively_utils.file_request import (
ws_request_file,
FileTypeNotAllowedError,
FileNotSelectedError,
FileUploadError,
)
async def on_form_submit(..., ws):
try:
file = await ws_request_file(form_id="...", name="...", ws, allowed_mimes=["image/png"])
except TimeoutError:
# Client did not upload a file in the given timeout
raise
except FileTypeNotAllowedError:
# Do something here
raise
except FileNotSelectedError:
# Do something here
raise
except FileUploadError:
# Catch-all for file upload related exceptions
raise
# No file.verify() needed - already called because allowed_mimes was passed
await file.async_save()
With real-time progress:
from duck.html.components.core.lively_utils.file_request import (
ws_request_file,
FileTypeNotAllowedError,
FileNotSelectedError,
FileUploadError,
)
async def on_progress(progress):
# Maybe update UI here
print(progress)
async def on_form_submit(..., ws):
try:
file = await ws_request_file(
form_id="...", name="...", ws,
allowed_mimes=["image/png"],
on_progress=on_progress,
)
except TimeoutError:
raise
except FileTypeNotAllowedError:
raise
except FileNotSelectedError:
raise
except FileUploadError:
raise
await file.async_save()
Note: Exception classes from
duck.http.fileuploadsandduck.html.components.core.lively_utils.file_requestrefer to the same thing. Runhelp(ws_request_file)to see all available configurations.
Custom Events¶
You can create custom Lively events with window.LIVELY_APPLICATION.createDuckEvent, dispatch them like any other DOM event, and bind them on the Python side.
// Create a custom event
const event = lively.createDuckEvent("ReportBounds", detail, false, true);
// Fire event at root level
document.dispatchEvent(event);
createDuckEvent(eventName, detailOverrides, addToDuckEvents = true, raw = true):
eventName— name of the event (e.g."ReportBounds").detailOverrides— data to attach to the event’sdetail.addToDuckEvents— whether to add the event to Duck’s tracked event list.raw— whentrue, Duck skips its default per-field processing (e.g. extractingvaluefrom abutton) and passesdetailthrough directly.
On the Python side, bind the event by name:
page.document_bind("ReportBounds", on_report_bounds, force_bind=True)
on_report_bounds receives the event just like any other bound handler — no special handling needed for it being custom.
Component Lifecycle¶
Whenever each component is created or added to the component tree, the following methods are executed:
on_root_finalized:¶
This will be called whenever a root component is finalized, meaning it is never going to change. Use the for doing staff on root components e.g.,
using document_bind on Page components.
Args:
root: This is the root component.
on_parent:¶
Called whenever a child component is added to a parent component.
Args:
parent: The parent component.
Other points to Note¶
Duck only keeps track of
props(attributes) andstyle(style attributes) that are initially set using Duck. This means that Duck will not touch any prop or style attributes set soley using Javascript and are never declared on Lively components.The
valueargument parsed to a Duck event handler is variable and can be any datatype depending on the event.Component traversal can be done using
root,parentorchildrenproperties.JS execution is done asynchronously so all code that is parsed in
LivelyWebSocketView.execute_jsorget_js_resultis awaited by default.DOM mutation/updates using Javascript like moving, adding or removing components without using Lively will cause issues and by default, this will be flagged in the browser.