Theming
Trellis includes built-in dark mode support with automatic OS theme detection and a complete set of design tokens for styling.
Quick Start
Use theme tokens for colors that adapt automatically to light/dark mode:
from trellis.app import theme
from trellis import html as h
h.Div(
style=h.Style(
background_color=theme.bg_surface,
color=theme.text_primary,
border=f"1px solid {theme.border_default}",
)
)
Theme tokens are CSS variable references that automatically update when the theme changes.
ThemeSwitcher Widget
Add a theme toggle button with ThemeSwitcher:
Theme Switcher
from trellis import component
from trellis.app import TrellisApp, ClientState
from trellis import widgets as w
@component
def MyApp():
state = ClientState.from_context()
with w.Column(gap=16):
with w.Row(gap=8, align="center"):
w.Label(text="Toggle theme:")
w.ThemeSwitcher()
w.Label(text=f"Mode: {state.theme.value}")
w.Label(text=f"Resolved: {'Dark' if state.is_dark else 'Light'}")
@component
def Main():
TrellisApp(app=MyApp)
App = Main
The switcher cycles through: System (follows OS) → Light → Dark → System...
ClientState API
Access theme state anywhere in your app:
from trellis.app import ClientState, ThemeMode
state = ClientState.from_context()
# Properties
state.theme_setting # ThemeMode.SYSTEM | LIGHT | DARK
state.theme # Actual theme: LIGHT or DARK
state.is_dark # True if dark mode active
state.is_light # True if light mode active
# Methods
state.toggle() # Cycle to next mode
state.set_mode(ThemeMode.DARK) # Set explicitly
Theme Tokens
Use theme tokens for colors that adapt to light/dark mode:
from trellis.app import theme
from trellis import html as h
h.Div(
style=h.Style(
background_color=theme.bg_surface,
color=theme.text_primary,
border=f"1px solid {theme.border_default}",
)
)
Available Tokens
| Category | Tokens |
|---|---|
| Backgrounds | bg_page, bg_surface, bg_surface_raised, bg_surface_hover, bg_input |
| Text | text_primary, text_secondary, text_muted, text_inverse |
| Borders | border_default, border_subtle, border_strong, border_focus |
| Accent | accent_primary, accent_primary_hover, accent_subtle |
| Semantic | success, warning, info, error, error_hover |
| Shadows | shadow_sm, shadow_md, shadow_lg |
| Focus | focus_ring_color |
Each token is a CSS variable reference (e.g., theme.text_primary → var(--trellis-text-primary)) that automatically updates when the theme changes.
Conditional Styling
For logic that depends on the current theme:
Conditional Styling
from trellis import component
from trellis.app import TrellisApp, ClientState
from trellis import html as h
from trellis import widgets as w
from trellis.app import theme
@component
def ThemeAwareCard():
state = ClientState.from_context()
with w.Card(style=h.Style(padding=16)):
w.Heading(text="Theme-Aware Card", level=3)
w.Label(
text=f"Current theme: {'Dark' if state.is_dark else 'Light'}",
color=theme.text_secondary,
)
w.ThemeSwitcher()
@component
def Main():
TrellisApp(app=ThemeAwareCard)
App = Main
How It Works
- Connection: Client sends OS theme preference in hello message
- Initialization:
TrellisAppcreatesClientStatewith detected theme - Rendering:
ThemeProvidersetsdata-themeattribute on root element - CSS Variables: Tokens resolve based on
[data-theme="light"]or[data-theme="dark"] - Updates: When OS theme changes (in System mode), client notifies server, triggering re-render