Skip to main content

Common Patterns

Patterns you'll use frequently when building Trellis applications.

Two-Way Binding with mutable()

For form inputs, use mutable() to create two-way bindings between state and widgets:

Two-Way Binding
from dataclasses import dataclass
from trellis import Stateful, component, mutable
from trellis import widgets as w

@dataclass
class FormState(Stateful):
name: str
email: str
subscribed: bool

@component
def Form():
state = FormState(name="", email="", subscribed=False)

with w.Column(gap=12):
w.Heading(text="Sign Up", level=2)

w.TextInput(
value=mutable(state.name),
placeholder="Name",
)
w.TextInput(
value=mutable(state.email),
placeholder="Email",
)
w.Checkbox(
checked=mutable(state.subscribed),
label="Subscribe to newsletter",
)

w.Divider()
w.Label(text=f"Name: {state.name}")
w.Label(text=f"Email: {state.email}")
w.Label(text=f"Subscribed: {state.subscribed}")

App = Form

The mutable() function captures a reference to the state property. When the widget value changes, the state updates automatically, which triggers a re-render.

Passing State via Props

State lives in one component but can be passed to children as props:

from trellis import component, state_var
from trellis import widgets as w

@component
def Header(name: str) -> None:
w.Heading(text=f"Welcome, {name}!")

@component
def App() -> None:
user_name = state_var("Alice")

with w.Column():
Header(name=user_name) # Pass state as prop
w.Label(text="Content goes here")

The Header component re-renders when user_name changes, even though it doesn't own the state.

Callbacks That Modify State

Pass callbacks that update state down to child components:

Callbacks
from trellis import component, state_var
from trellis import widgets as w

@component
def Controls(on_increment, on_decrement, on_reset):
with w.Row(gap=8):
w.Button(text="-", on_click=on_decrement)
w.Button(text="+", on_click=on_increment)
w.Button(text="Reset", on_click=on_reset, variant="outline")

@component
def App():
count = state_var(0)

def increment():
nonlocal count
count += 1

def decrement():
nonlocal count
count -= 1

def reset():
nonlocal count
count = 0

with w.Column(gap=12):
w.Heading(text=f"Count: {count}", level=2)
Controls(
on_increment=increment,
on_decrement=decrement,
on_reset=reset,
)

App = App

This pattern keeps state management centralized while letting child components trigger updates.

Conditional Rendering

Use Python's if statements to conditionally render elements:

@component
def App() -> None:
state = AppState()

with w.Column():
if state.is_loading:
w.Label(text="Loading...")
elif state.error:
w.Label(text=f"Error: {state.error}", color="red")
else:
w.Label(text=f"Data: {state.data}")
Conditional Rendering
from trellis import component, state_var
from trellis import widgets as w

@component
def App():
show_details = state_var(False)

def toggle_details():
nonlocal show_details
show_details = not show_details

with w.Column(gap=12):
w.Heading(text="Product", level=2)
w.Label(text="A great product for all your needs.")

w.Button(
text="Hide Details" if show_details else "Show Details",
on_click=toggle_details,
)

if show_details:
with w.Card():
w.Label(text="Price: $99.99")
w.Label(text="In stock: Yes")
w.Label(text="Ships in 2-3 days")

App = App

Lists

Render lists by iterating with for:

@component
def ItemList() -> None:
items = ["Apple", "Banana", "Cherry"]

with w.Column():
for item in items:
w.Label(text=item)

Dynamic Lists with State

Dynamic List
from dataclasses import dataclass
from trellis import Stateful, component
from trellis import html as h
from trellis import widgets as w

@dataclass
class ListState(Stateful):
items: list[str]
counter: int

def add(self):
self.items.append(f"Item {self.counter}")
self.counter += 1

def remove(self, item: str):
self.items.remove(item)

@component
def App():
state = ListState(items=["First item", "Second item"], counter=3)

with w.Column(gap=12):
w.Heading(text="Items", level=2)

if not state.items:
w.Label(text="No items yet.", color="#888")
else:
for item in state.items:
with w.Row(gap=8, align="center"):
w.Label(text=item, style=h.Style(flex=1))
w.Button(text="Remove", on_click=lambda i=item: state.remove(i), variant="ghost")

w.Button(text="Add Item", on_click=state.add)

App = App

The Lambda Capture Pattern

When creating callbacks in a loop, capture the loop variable with a default argument:

# Wrong - all callbacks use the last value of `item`
for item in items:
Button(text="Remove", on_click=lambda: remove(item))

# Correct - each callback captures its own value
for item in items:
Button(text="Remove", on_click=lambda i=item: remove(i))

The i=item creates a new binding for each iteration.

Keys for List Items

When rendering dynamic lists, use .key() to help Trellis track items across renders:

@component
def ItemList(items: list[str]) -> None:
for item in items:
with h.Div().key(item): # Key by the item's unique identifier
h.Span(item)
Button(text="Remove", on_click=lambda i=item: remove(i))

Keys are important because:

  • They help Trellis match elements when items are reordered, added, or removed
  • Without keys, Trellis uses position, which can cause incorrect state preservation
  • Keys should be stable and unique within the list (IDs, not indices)

Component Composition

Break complex UIs into smaller components:

Composition
from dataclasses import dataclass
from trellis import Stateful, component
from trellis import html as h
from trellis import widgets as w

# Reusable stat display
@component
def Stat(label: str, value: str):
with w.Row(gap=8):
w.Label(text=f"{label}:", bold=True)
w.Label(text=value)

@dataclass
class DashboardState(Stateful):
users: int
revenue: int

def refresh(self):
self.users += 10
self.revenue += 100

@component
def Dashboard():
state = DashboardState(users=1234, revenue=56789)

with w.Column(gap=16):
w.Heading(text="Dashboard")

with w.Card():
Stat(label="Total Users", value=str(state.users))
Stat(label="Active Today", value="89")

with w.Card():
Stat(label="Total", value=str(state.revenue))
Stat(label="This Month", value="$12,345")

w.Button(text="Refresh Data", on_click=state.refresh)

App = Dashboard

Responding to Theme Changes

Access the current theme and style components accordingly:

Theme-Aware Component
from trellis import component
from trellis import html as h
from trellis.app import TrellisApp, ClientState, theme
from trellis import widgets as w

@component
def ThemeAwareCard():
state = ClientState.from_context()

with w.Card(style=h.Style(
background_color=theme.bg_surface,
border=f"1px solid {theme.border_default}",
padding=16,
)):
w.Label(
text=f"Current theme: {'Dark' if state.is_dark else 'Light'}",
color=theme.text_primary,
)
w.Button(text="Toggle Theme", on_click=state.toggle)

@component
def Main():
TrellisApp(app=ThemeAwareCard)

App = Main

Use theme tokens for colors that adapt automatically, and ClientState.from_context() when you need conditional logic based on the active theme.

Next Steps