Skip to main content

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) → LightDarkSystem...

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

CategoryTokens
Backgroundsbg_page, bg_surface, bg_surface_raised, bg_surface_hover, bg_input
Texttext_primary, text_secondary, text_muted, text_inverse
Bordersborder_default, border_subtle, border_strong, border_focus
Accentaccent_primary, accent_primary_hover, accent_subtle
Semanticsuccess, warning, info, error, error_hover
Shadowsshadow_sm, shadow_md, shadow_lg
Focusfocus_ring_color

Each token is a CSS variable reference (e.g., theme.text_primaryvar(--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

  1. Connection: Client sends OS theme preference in hello message
  2. Initialization: TrellisApp creates ClientState with detected theme
  3. Rendering: ThemeProvider sets data-theme attribute on root element
  4. CSS Variables: Tokens resolve based on [data-theme="light"] or [data-theme="dark"]
  5. Updates: When OS theme changes (in System mode), client notifies server, triggering re-render