Lively UI

Reactive Python UI Without JavaScript

Badge

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)

  1. You create a Page class in Python.

  2. You add components (like buttons, text, forms).

  3. You attach Python functions to events (like button clicks).

  4. 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_targets when 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)

Fast Navigation

  • URL paths returning Component responses allow vdom-diffing for minimal DOM updates.

  • Use duckNavigate for fast page navigation.

  • All links do partial page reload whenever possible. Exempt links from partial page reloading by setting data-no-duck attribute.

  • The default window.open has been altered to use fast navigation whenever possible.

  • Whenever a user want to visit a next page, whenever possible, fast navigation is used. If you want to override this, let’s say you want the user to apply new page headers, you can set fullpage_reload or fullpage_reload_headers (_headers that force a fullpage reload when set on Page's request_) on Page components.


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_COMPONENTS in 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 component style.

    • StyleCompatibilityExtension: This enables compatibility of style properties like setting backdrop-filter will also set the -webkit-backdrop-filter and 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 tracked

  • Untracked 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:

update_now

sync_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.fileuploads and duck.html.components.core.lively_utils.file_request refer to the same thing. Run help(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’s detail.

  • addToDuckEvents — whether to add the event to Duck’s tracked event list.

  • raw — when true, Duck skips its default per-field processing (e.g. extracting value from a button) and passes detail through 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) and style (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 value argument parsed to a Duck event handler is variable and can be any datatype depending on the event.

  • Component traversal can be done using root, parent or children properties.

  • JS execution is done asynchronously so all code that is parsed in LivelyWebSocketView.execute_js or get_js_result is 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.