Theming¶
Duck’s theme system provides design tokens (colors, spacing, typography, etc.) as CSS
custom properties, so components reference var(--theme-<token>) instead of hardcoded
literals.
The Theme class¶
A Theme is an extensible set of design tokens.
from duck.html.components.theme import Theme
theme = Theme(name="dark")
Every Theme starts with Theme.DEFAULTS — a base set of tokens (accent_color,
border_radius, font_family, and so on). You can layer, override, or add tokens on top
of that.
Creating and extending themes¶
# Override or add tokens at construction time
theme = Theme(name="dark", accent_color="#F5C842", border_color="rgba(255,255,255,0.12)")
# Layer on top of an existing theme
brand_theme = Theme(name="brand", base=theme, accent_color="#00FFAA")
# Or derive a new theme from an existing one
brand_theme = theme.extend("brand", accent_color="#00FFAA")
# Add or override tokens after construction
theme.update(spacing="12px", font_size="1.1rem")
Reading tokens¶
Tokens read like plain attributes:
theme.accent_color # "#F5C842"
Tokens can not be set via attribute assignment — theme.accent_color = "..." raises
AttributeError. Use update() or the constructor instead.
get() is the non-raising lookup, with an optional default:
theme.get("accent_color") # "#F5C842"
theme.get("nonexistent", "fallback") # "fallback"
The dynamic flag¶
Every Theme instance has a dynamic flag (default False) that changes what attribute
reads and get() return:
|
|
|---|---|
|
the literal value — |
|
the CSS var name — |
This lets the same attribute read work in two different contexts: pull the literal value when you need it directly, or drop the CSS var reference straight into markup/styles when you want the value to live-update from the stylesheet.
theme = Theme(name="dark", dynamic=True)
theme.accent_color # "--theme-accent-color"
theme.dynamic = False
theme.accent_color # "#F5C842"
The flag can be flipped at any time on an existing instance — it’s not fixed at construction.
Two ways to control this per-lookup instead of flipping the instance flag:
theme.var("accent_color") # "--theme-accent-color" (always, regardless of dynamic)
theme.get("accent_color", dynamic=True) # "--theme-accent-color" (this call only)
theme.get("accent_color") # "#F5C842" (follows the instance's dynamic flag)
Generating CSS¶
theme.to_css_vars()
# {"--theme-accent-color": "#F5C842", "--theme-border-radius": "12px", ...}
theme.to_style() # Style component declaring vars under :root
theme.to_style(".theme-dark") # scoped to a subtree instead of :root
to_css_vars() and to_style() always emit literal values, regardless of the
dynamic flag — a CSS custom property can’t declare itself as its own var reference.
The active theme¶
Theme.current is a class-level, globally active theme:
Theme.current = dark_theme
active = Theme.current
Components and the Page component (see below) read Theme.current unless told
otherwise.
Page component auto-theming¶
The Page component can automatically inject the active theme’s CSS. This is controlled
by the add_theme_css argument, which defaults to True:
Page(...) # add_theme_css=True by default — theme CSS is injected
Page(..., add_theme_css=False) # opt out — no theme CSS is added
With the default True, Page adds a <style> block (via Theme.current.to_style())
to the document head automatically — you don’t need to call to_style() yourself for the
common case of “use the global theme everywhere.”
Page-specific theming (not yet supported)¶
There’s no built-in per-page theming yet — theming is global-only, via Theme.current.
If you need a page to look different, you can approximate page-specific theming manually:
Create your global theme as usual, with
dynamic=True:Theme.current = Theme(name="global", dynamic=True, accent_color="#F5C842")For the page that needs different values, create a separate
Themeinstance (alsodynamic=True) and add its CSS to that page directly — for example withpage.add_to_head(page_theme.to_style())— instead of assigning it toTheme.current.page_theme = Theme(name="landing", dynamic=True, accent_color="#00FFAA") page.add_to_head(page_theme.to_style())
Do not set Theme.current to the page-specific theme. Theme.current is global, so
doing that would change the theme for every page, not just the one you’re working on. Set
add_theme_css=False on that Page if you don’t want the global theme’s CSS injected
alongside your page-specific CSS.
This is a manual workaround, not a first-class feature — true page-scoped theming isn’t implemented yet.