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:
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:
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}")
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
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:
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:
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
- Theming — Dark mode, theme tokens, and styling
- Design Overview — How Trellis works under the hood
- State Management — Deep dive into the state system
- UI and Rendering — How rendering works