Skip to content

Themes, variants, and scoped styles

Stability

Classifications for this surface are recorded in STABILITY.md. Package maturity (Beta/Alpha) is separate from API level (beta / experimental / internal / deferred).

Status: Accepted

from hedron import Hedron

app = Hedron(
    title="Themed app",
    theme="aurora",
    security="standard",
    session_secret="replace-in-production",
)

return Article(
    class_=styles.root,
    children=[Heading(class_=styles.title)],
)

Scoped style symbols

A component styles.css exposes local classes through a declared styles binding. Unknown names fail before production. Local classes and keyframes compile to stable identifiers; :global(...) is explicit.

Themes

Theme declares semantic CSS variables and component variant defaults. Applications may register themes and select one globally or at supported boundaries. Themes must define required accessibility tokens and may extend, but not silently remove, base contracts.

In 0.60, Theme.variants is an explicit finite mapping of token overrides. Variants are additive, validated, and emitted only when selected; they do not create a private selector API or change component behavior.

Hedron ships two complete themes. default is the quiet blue product baseline; aurora uses a more expressive violet palette, tighter geometry, richer depth, and a two-tone ambient background. Both include explicit light and dark palettes and follow the browser preference when no color mode is forced.

app = Hedron(theme="aurora")

# An individual page can override the app selection for previews or mounted surfaces.
return Page(content, data_hedron_theme="default", data_theme="dark")

Use StyleScope(variant=...) for a subtree:

from hedron import StyleScope, Text

StyleScope(Text("Dense review surface"), theme="aurora", variant="dense")

Unknown variant names fail closed. The base theme remains the fallback when no variant is selected.

Theme(
    name="acme",
    tokens={"color.accent": "#..."},
    content_width="wide",
    typography_features={"kern": 1, "liga": 1},
    typography_role_features={
        "code": {"zero": 1},
        "tabular": {"tnum": 1},
    },
)

content_width is independent from nav_width and accepts narrow, default, wide, full, or a validated CSS length. OpenType feature tags contain exactly four ASCII letters/digits and values are integers from 0 through 99; booleans are rejected. Global features inherit through the document, while the finite body, display, code, and tabular role maps provide scoped overrides. Theme exports and data-only theme packages preserve these structured fields during round trips.

Built-in presentation

Hedron() includes a local, responsive stylesheet for typography, spacing, forms, buttons, cards, tables, navigation, status states, and the built-in layout components. It supports light and dark system preferences and does not require a build step or a remote stylesheet.

The baseline lives in the low-priority base cascade layer. Application styles, component styles, semantic theme variables, and the overrides layer can all customize it without copying the stylesheet. To start with an entirely unstyled document, disable it at the application boundary:

app = Hedron(
    title="Unstyled shell",
    default_styles=False,
    security="standard",
    session_secret="replace-in-production",
)

This switch disables only Hedron's baseline stylesheet. It does not remove application CSS, component CSS, or registered theme assets.

Runtime user data cannot become raw CSS. Dynamic presentation uses declared variants, safe attributes, or validated CSS-variable values. Strict mode can reject inline style attributes and remote resources.

0.60 also adds tested modern-color fallbacks, preference-aware tokens, and selected Progressive light-dark() / @property enhancements. Every enhanced declaration has a canonical fallback; remote font and asset fetching remains outside the Supported path.

Applications override packaged presentation through semantic props, extra classes, custom properties, cascade override layers, or ejected component styles.

Custom theme platform (0.60)

The canonical 0.60 authoring path is an immutable ThemeSpec; ThemeBuilder is only a convenience facade. Absolute CSS Color 4 inputs are accepted through Color.parse() or the declared constructors and always have a deterministic sRGB fallback.

from hedron import Color, ThemeBuilder, package_theme, validate_theme_spec

spec = (
    ThemeBuilder("acme")
    .token("color.bg", "#ffffff")
    .token("color.fg", "#111827")
    .token("color.muted", "#4b5563")
    .token("color.focus", Color.oklch(0.62, 0.16, 260))
    .token("font.family", "system-ui")
    .token("font.size", "1rem")
    .token("space.unit", "0.25rem")
    .accessibility("forced-colors", **{"color.focus": "Highlight"})
    .build()
)
report = validate_theme_spec(spec)
package = package_theme(spec, licenses=("MIT",))

load_theme_package() verifies the archive contents, hashes, fingerprint, and validation digest before register_theme_package() bridges the data-only spec to the existing Theme registry. diff_theme_specs(), explain_theme_spec(), and conformance_report() are read-only deterministic services shared by tooling and package checks. The CLI exposes the same path through hedron style init, hedron style conform, and hedron style package.