Styling and themes

Style

A Style is an immutable, typed set of CSS declarations. None means “unspecified” (never CSS none). a | b merges right-biased; nested pseudo-state and responsive styles merge recursively.

base = Style(color="on_primary", background="primary", padding=12, border_radius="md")
button = base | Style(font_weight=600, hover=Style(background="primary_hover"))

Every element ends up with one generated class (rw-<hash>), so there are no specificity battles; identical styles share a class across the page.

Values

  • Numbers on length properties become pixels (padding=12 → 12px; 0 → 0).

  • Scale tokens: spacing xs sm md lg xl 2xl 3xl, radius none sm md lg xl full, font sizes xs … 4xl, shadows sm md lg, fonts body heading mono.

  • Theme colour names (primary, on_surface, muted …) become CSS variables, so light/dark switching needs no re-render.

  • Shorthands: layout="vertical"|"horizontal", padding_x/padding_y, margin_x/margin_y, columns=3 (equal grid tracks), border=True, transition=True.

  • Anything else: raw={"text-underline-offset": "3px"}.

Values containing ; { } < > or /* are rejected when the Style is created, so the error points at your line and CSS injection is impossible.

States and responsiveness

Style(
    padding=24,
    hover=Style(background="surface_alt"),     # wrapped in @media (hover: hover)
    focus_visible=Style(outline="2px solid"),
    disabled=Style(opacity=0.5),               # :disabled and [aria-disabled=true]
    viewport={"mobile": Style(padding=12)},    # @media
    container={"compact": Style(layout="vertical")},  # @container
)

Viewport breakpoints (overridable with App(breakpoints=...)):

mobile

below 640px

tablet

640px – 1023px

narrow

below 1024px

desktop

1024px and up

sm

640px and up

md

768px and up

lg

1024px and up

xl

1280px and up

Container sizes: compact (below 480px), regular (480px and up), wide (900px and up). A container must establish itself with Style(container_type="inline-size").

Python never inspects the viewport: the browser evaluates the generated queries.

Themes

App(theme=...) takes a preset — modern, minimal, soft, professional — or a Theme. theme_mode is "system" (follows the OS), "light" or "dark". Customize a preset:

brand = MODERN.with_colors(light={"primary": "#ff0066"}, dark={"primary": "#ff5c9a"})
app = App(theme=brand)

Every theme includes a reset, a visible :focus-visible ring and prefers-reduced-motion support.

Layout primitives

Stack, Row and Column wrap flexbox; Row(collapse="mobile") stacks vertically below a breakpoint. Grid(min_column_width=240) fits as many columns as the space allows without media queries; Grid(columns=3) collapses to one column on mobile. Card, Container, Spacer, Divider and the semantic Header, Main, Footer, Nav, Section, Article, Aside complete the set.

Badge("new", tone="success") labels statuses, tags and counts (tones: neutral, primary, success, warning, danger). Avatar("Ada Lovelace") shows a picture, or the person’s initials when there is no src.