Skip to content

Presentation · themes · CSS boundaries

Style the whole interface, one vocabulary at a time

Want to try the styles? Open the interactive Styles Explorer to compare themes, browse real components, inspect tokens, and export CSS without a backend. For the visual QA app with all the appearance and emphasis buttons, open the Component Explorer.

Hedron styling starts with semantic Python props and ends as ordinary HTML, CSS, and data-hedron-* markers. Use the built-ins for the common 80%, a DesignSystem for brand-wide decisions, and scoped CSS when a component genuinely owns a visual detail.

This page is a visual tour of the styling surface: see the result first, then copy the smallest useful Python pattern.

What is new in 0.60

0.60 completes the custom-theme platform on top of the 0.59 CSS foundation: absolute CSS Color 4 values with deterministic sRGB fallbacks, immutable ThemeSpec / ThemePatch authoring, registry-derived validation profiles, accessibility-mode mappings, bounded recipe families, server-first theme preferences, and zero-application-CSS contracts for the remaining product surfaces. The complete capability matrix—including the 0.59 CSS foundation and its Progressive, Experimental, and Deferred tiers—is in Modern CSS in 1.0.

The 0.60 styling upgrade in one view

Need 0.60 path What remains application-owned
Use a brand palette DesignSystem.brand() or a registered Theme Product identity and content
Author a reusable theme Immutable ThemeSpec through ThemeBuilder Which tokens and visual decisions to publish
Layer a bounded change ThemePatch / ThemeSpec.apply_patches() Review and approval of the change
Validate a theme validate_theme_spec() or style conform The profile that matches the package surface
Ship a theme Data-only package_theme() archive License metadata and distribution
Select a preference ThemePreference + ThemePicker Persistence, authorization, and the POST route
Style product chrome and workflows Brand, ToastHost, ConnectorFlow, ScrollRegion Content, state, and actions

The rule is simple: use the smallest bounded contract that expresses the intent. A custom theme may add tokens, modes, accessibility mappings, and presentation recipes; it may not add behavior, private selector APIs, remote assets, or arbitrary CSS values.

The styling stack

There are four layers. Start at the top and move down only when the layer above cannot express the intent.

Four layers, one cascadeintent → tokens → components → local detail
Design systembrand, density, motion
Presentation propsappearance, size, gap
  <span class="sg-recipe-arrow" aria-hidden="true">→</span>
  <div class="sg-recipe-node"><strong>Scoped CSS</strong><small>component-owned detail</small>
</div>

Layer Use it for Examples
DesignSystem / Theme Brand and app-wide decisions Accent palette, geometry, typography, dark mode
Shared presentation props Repeated component intent appearance="soft", density="compact", gap="lg"
StyleRecipe Semantic feature defaults primary_action, form_surface, metadata
Component styles.css A component's unique visual language A callout rail, a chart legend, a custom animation

The shortest path

If you are styling a built-in, look for a semantic prop first. If several features need the same combination of props, make a recipe. If the style belongs to one component and has no useful semantic vocabulary, colocate scoped CSS with that component.

Hedron styling and CSS: the crosswalk

The table below maps Hedron's public styling features to the CSS concepts they compile to or intentionally stand in for. It describes the Hedron 0.60 contract, not the support level of a particular browser. Not implemented means that Hedron does not currently own a public API or styling contract for the CSS feature; it is not a promise that an arbitrary application stylesheet can safely use it.

Area Hedron feature Equivalent CSS feature(s) Hedron status Boundary or fallback
Foundation default_styles=True Bundled reset, base rules, component selectors, custom properties Supported Disable with default_styles=False; application and component CSS remain separate.
Foundation Shared presentation props: size, density, appearance, emphasis, width, shape data-hedron-* selectors; color, background, border, border-radius, box-shadow, sizing Supported Closed vocabularies keep themes and components interoperable.
Foundation Controls: Button, LinkButton, IconButton inline-flex, padding, line-height, :hover, :focus-visible, :disabled Supported Native control semantics and focus behavior remain authoritative.
Foundation Form controls and validation states appearance, field sizing, :focus-visible, :invalid, error/help selectors Supported Labels, errors, and native keyboard behavior are not styling concerns.
Foundation Surface and Card background, border, border-radius, box-shadow, padding Supported appearance, padding, density, and elevation are finite props.
Foundation Alert, Badge, Status, StateView Semantic color/border/background plus non-color state indicators Supported Tone never relies on color alone; loading/error/empty branches stay complete.
Foundation Typography roles on Text, Heading, and Typography font-family, font-size, font-weight, line-height, letter-spacing Supported Use roles instead of arbitrary per-element font sizes.
Foundation Modern typography and local font assets text-wrap: balance, text-wrap: pretty, hyphens, overflow-wrap, font-size-adjust, variable fonts Supported System fallback stacks and ordinary wrapping remain the compatibility path.
Foundation overflow and lines= overflow, overflow-wrap, word-break, text-overflow, line clamping Supported wrap, break, truncate, and clip are explicit; clamping never invents content.
Layout Stack and Inline Flexbox: display: flex, flex-direction, align-items, gap Supported Named gap tokens are CSP-safe.
Layout Grid, FormGrid, and GridItem CSS Grid: grid-template-columns, grid-column, gap Supported Breakpoint maps are mobile-first and preserve DOM/read order.
Layout Container with query="inline-size" container-type: inline-size, container-name, @container Supported Viewport/static layout remains the fallback.
Layout Responsive maps: base, sm, md, lg, xl @media (min-width: ...) Supported Breakpoints are finite; arbitrary media-query strings are not a Python API.
Layout gap, padding, track, width, and content-width tokens gap, padding, max-inline-size, inline-size, minmax() Supported Unsupported lengths fail diagnostically instead of silently remapping.
Layout SplitView, MasterDetail, PageHeader, ActionGroup Grid/flex tracks, minmax(), clamp(), align-*, justify-* Supported Components own responsive collapse and preserve source order.
Layout Responsive Table policies overflow, responsive table wrappers, @media layout adjustments Supported scroll, stack, and priority are bounded policies; table semantics remain intact.
Layout ScrollRegion overflow: auto, logical overflow, scrollbar affordance Supported Bounded scrolling must not replace list, table, log, or timeline semantics.
Layout Intrinsic and fluid sizing min(), max(), clamp(), minmax(), fit-content(), aspect-ratio Supported Ordinary lengths and static layout provide the compatibility path.
Layout Dynamic viewport and safe-area behavior svh, lvh, dvh, env(safe-area-inset-*) Supported Static viewport and zero-inset fallbacks remain usable.
Layout Logical and bidirectional layout Logical properties/values, direction, writing-mode, margin-inline, inset-block Supported Physical properties are limited to documented exceptions.
Layout Grid alignment enhancement subgrid for grid-template-columns / grid-template-rows Progressive Ordinary independent Grid/FormGrid tracks are the fallback.
Theme Theme and DesignSystem tokens CSS custom properties such as --color-*, --space-*, --font-* Supported Tokens are the dynamic theming path; values are validated and emitted deterministically.
Theme ThemeBuilder, immutable ThemeSpec, ThemePatch Canonical custom-property declarations and finite token overrides Supported Theme packages are data-only; no executable hooks or runtime CSS injection.
Theme Light/dark modes and ColorMode [data-theme], explicit mode selectors, @media (prefers-color-scheme: dark) Supported Explicit markers and duplicated token rules are the baseline.
Theme Finite theme variants [data-hedron-variant="..."] and additive custom-property overrides Supported Variant names are validated; arbitrary selectors are not generated from user input.
Theme StyleScope Inherited custom properties and scoped data-hedron-* markers Supported Scope changes presentation only; it does not change behavior or DOM order.
Theme StyleRecipe Reusable selector/property combinations resolved before rendering Supported Recipes are family-scoped and explicit component props win.
Theme Server-first ThemePreference and ThemePicker Theme/color-mode markers, custom-property selection, color-scheme hints Supported The server owns allowlisting, persistence, and authorization; the optional boot helper only reduces flash.
Theme Brand geometry, density, elevation, motion, and navigation presets border-radius, spacing variables, box-shadow, transition-duration, width variables Supported Presets compile to bounded tokens, not arbitrary CSS declarations.
Theme Modern absolute colors and generated palettes color-mix(), hwb(), lab(), lch(), oklab(), oklch(), color() Supported A canonical sRGB fallback precedes enhanced wide-gamut output.
Theme Accessibility theme modes @media (forced-colors: active), prefers-contrast, prefers-reduced-transparency Supported Semantic focus, contrast, print, and non-color fallbacks remain required.
Compiler Component styles.css and StyleSymbols Local class/keyframe names, :global(...), deterministic source maps Supported Local symbols are rewritten; remote resources and unsafe globals are rejected.
Compiler CSS grammar-aware compilation Selectors, declarations, descriptors, strings, comments, URLs, custom identifiers Supported Malformed or ambiguous syntax fails during the build, not at runtime.
Compiler Nested component CSS CSS Nesting Level 1, &, nested conditional rules Supported Hedron emits browser-valid scoped CSS.
Compiler Conditional and future at-rules @media, @supports, @container, @scope, @starting-style Supported Safe syntax is preserved; unsafe or ambiguous rewrites are rejected.
Compiler Local styles and assets @import, url(), @font-face, image-set() Supported Registered local assets resolve at build time; remote imports do not.
Cascade Hedron layer order @layer reset, tokens, base, components, utilities, overrides Supported One deterministic cascade authority prevents competing theme registries.
Cascade Specificity containment and relational selectors :where(), :is(), :not(), :has(), specificity normalization Supported Public semantic markers are preferred over private selector theming.
Cascade Native scope enhancement @scope, :scope Progressive Hedron's marker/token boundary is the compatibility fallback.
Overlay Dialog, Popover, ContextMenu, Tooltip, ToastHost <dialog>, Popover API, :popover-open, ::backdrop, top layer, z-index tokens Supported Native/details/static flow remains usable when enhanced APIs are unavailable.
Overlay Logical overlay placement and collision inset-*, position-area, position-try-fallbacks Progressive Hedron's finite placement vocabulary falls back to logical flow or bounded positioning.
Product surfaces AppShell, Brand, AppFooter, ConnectorFlow, and workflow chrome Grid/flex shell layout, logical positioning, overflow, status and connector selectors Supported Components render presentation state supplied by the application; they do not own auth, execution, polling, or authorization.
Motion Built-in motion presets and reduced motion transition, animation, @media (prefers-reduced-motion: reduce) Supported motion="none" and preference media produce a stable, immediate presentation.
Motion Entry/exit transitions @starting-style, transition-behavior: allow-discrete Progressive Open/closed state changes remain correct without the enhancement.
Motion View-transition enhancement view-transition-name, view-transition-class, View Transitions API Progressive Ordinary navigation or HTMX swaps preserve focus, history, and server state.
Motion Scroll-driven animation animation-timeline, scroll(), view(), timeline-scope Experimental Decorative only; static complete presentation is the fallback.
Media Interaction preferences prefers-reduced-motion, forced-colors, prefers-contrast, hover, pointer Supported The non-enhanced path remains keyboard-usable and non-color-dependent.
Media Print presentation @media print, break-*, print-color-adjust Supported Source order, links, forms, data, statuses, and page breaks remain readable.
Performance Containment and deferred rendering contain, content-visibility, contain-intrinsic-size Progressive Fully rendered content is the fallback; use only in benchmarked surfaces.
Unimplemented CSS Style queries @container style(...) Progressive No public Hedron style-query API; internal adaptation is bounded, with semantic markers and inherited tokens as the fallback.
Unimplemented CSS CSS masonry masonry track sizing / masonry layout Not implemented Use ordinary Grid; masonry is outside the 0.60 contract.
Unimplemented CSS Houdini worklets paint(), layout(), Paint/Layout Worklets Not implemented Hedron does not add a browser-execution boundary for styling.
Unimplemented CSS Free-form styling from Python Arbitrary inline style, property-value DSLs, utility-string classes Not implemented Use semantic props, tokens, recipes, or component styles.css.
Unimplemented CSS Runtime style injection Client-side <style> injection or runtime CSS compilation Not implemented Production CSS is built, fingerprinted, and served as an asset.
Unimplemented CSS Remote style dependencies Remote @import, automatic remote fonts, implicit remote assets Not implemented Register and fingerprint local assets explicitly.
Unimplemented CSS Private selector theming Arbitrary descendant/private selectors as a theme API Not implemented Use public parts, slots, tokens, markers, and documented component contracts.

For the machine-readable capability inventory, including gates and exact fallback language, see modern-css-inventory-059.toml and the Modern CSS in 1.0 guide.

1. Start with the shared vocabulary

The presentation vocabulary is intentionally finite. That gives themes one predictable set of hooks and makes invalid combinations fail early instead of becoming one-off CSS.

Palette seedaccent="#087f75"
accent#087f75
accent-soft#dff4ef
fg#12212b
muted#60727d
surface#f3f7f7
danger#c2413b

The most-used values are:

size sm · md · lg density compact · comfortable · spacious appearance solid · outline · soft · ghost emphasis primary · secondary · danger · neutral overflow wrap · break · truncate · clip gap none · xs · sm · md · lg · xl

Controls: appearance and emphasis are separate

appearance describes the treatment; emphasis describes the meaning. Keeping them separate lets one theme make a primary action solid, a secondary action outlined, and a danger action high-contrast without inventing a new prop for every combination.

Control galleryButton
Primary Secondary Soft Ghost Danger Disabled
Small Medium Large
from hedron import Button

Button("Save changes", appearance="solid", emphasis="primary", size="md")
Button("Cancel", appearance="outline", emphasis="secondary")
Button("Delete workspace", appearance="solid", emphasis="danger")

Legacy variant="primary|secondary|danger" remains supported. Prefer the shared appearance / emphasis vocabulary when you want the full presentation system to coordinate the result.

2. Build hierarchy with layout primitives

Layout components keep spacing and responsive behavior in the same vocabulary as the rest of the design. Named gaps are CSP-safe and preserve DOM order.

Layout galleryStack · Inline · Grid
Stack(gap="sm")
Heading Description Actions
Inline(gap="md")
Filter Sort Export
Grid(columns=3)
ABC DEF
GridItem(span={2})
Wide itemSide ABC
from hedron import Grid, GridItem, Heading, Inline, Stack, Text

Stack(
    Heading("Deployment history", level=2),
    Text("Recent releases and their current state."),
    Inline("Filter", "Export", gap="sm"),
    gap="lg",
)

Grid(
    GridItem(Text("Overview"), span={"base": 1, "md": 2}),
    Text("Activity"),
    columns={"base": 1, "md": 3},
    gap="md",
)

Grid and FormGrid accept breakpoint maps. The base value is the mobile-first default; sm, md, lg, and xl progressively enhance it. GridItem changes span, not reading order.

3. Give surfaces depth without hand-written CSS

Use Surface for a visual grouping and Card when you also need header/body/footer slots. Appearance, padding, density, and elevation are explicit, inspectable choices.

Surface galleryplain · raised · soft · danger
Plain surfaceQuiet grouping with a border.
Raised surfaceUse for a panel above the page.
Soft surfaceUse for selected or contextual content.
Danger surfaceReserve strong treatment for risk.
Compact densityMore information per viewport.
Spacious densityMore breathing room for focus.
from hedron import Card, Surface, Text

Surface(
    Text("Queue is healthy", role="body"),
    appearance="raised",
    padding="lg",
    elevation="md",
)

Card(
    Text("42 deployments this month"),
    title="Release activity",
    appearance="raised",
    padding="md",
    elevation="sm",
)

The shared components own their baseline CSS. You can still add class_ for a local hook, but a class should refine a semantic component rather than replace it.

4. Treat typography as a role, not a font-size hunt

Text, Heading, and Typography accept roles such as display, title, body, label, caption, and mono. Roles let themes retune hierarchy globally.

Type scalerole=display → caption
42,018display · metric
Release activitytitle · heading
The queue is processing normally.body · paragraph
Updated 2 minutes agocaption · metadata
from hedron import Heading, Text

Heading("Release activity", level=2, role="title")
Text("42,018", role="display", as_="strong")
Text("Updated 2 minutes ago", role="caption", overflow="truncate")

Keep the native heading level logical. Use role to change its visual treatment; do not skip from h2 to h5 because the smaller size looks right.

Overflow is also a design decision

Use wrap for ordinary copy, break for long identifiers, truncate for compact single-line metadata, and clip only when the content is intentionally decorative. When lines= is supplied, it creates a bounded multi-line clamp.

5. Make forms feel like part of the same system

FormGrid gives fields a responsive column map. Pair it with FormField, labels, and the built-in controls so focus rings, spacing, and validation states stay consistent.

Responsive form galleryFormGrid(columns={"base": 1, "md": 2})
Shown to members on the workspace switcher.
Save workspaceCancel
from hedron import FormField, FormGrid, TextInput

FormGrid(
    FormField(
        name="name",
        label="Workspace name",
        control=TextInput("name", required=True),
    ),
    FormField(
        name="region",
        label="Region",
        control=TextInput("region", value="us-east"),
    ),
    columns={"base": 1, "md": 2},
    gap="md",
)

For a complete POST, add Form, a CSRF field, and SubmitButton. Styling should never be the reason to remove a native label, focusable control, or validation message.

6. Use status and state components consistently

Tone carries meaning; appearance carries treatment. A success badge and a success alert can share the same semantic tone while taking different amounts of attention.

State gallerytone + size + appearance
NeutralInfoSuccessWarningDanger
HealthyAll workers are responding.
Needs attentionOne connector is delayed.
Action requiredCredentials expired.
from hedron import Alert, Badge, Status

Badge("Succeeded", tone="success", size="sm")
Alert("The connector needs new credentials.", tone="danger", appearance="soft")
Status("Processing", tone="info", size="sm")

For full-page loading, empty, permission, offline, and error branches, use StateView. Make every branch a valid and styled state; HTMX swaps should not turn a polished screen into an unstyled fragment.

7. Create a brand with DesignSystem

DesignSystem.brand() turns a small set of inputs into a coordinated Theme with light/dark palettes, accessibility checks, geometry, density, typography, motion, elevation, and navigation width.

from hedron import DesignSystem, Hedron, StyleRecipe

design = DesignSystem.brand(
    "northstar",
    accent="#2563eb",
    geometry="soft",
    typography="system-sans",
    density="comfortable",
    elevation="subtle",
    motion="calm",
    navigation="default",
)

app = Hedron(
    title="Northstar",
    theme=design,
    session_secret="replace-in-production",
)

The accent is a safe color seed, not arbitrary CSS. Hedron can adjust a generated value when needed to satisfy the required contrast pairs and records that adjustment in the design plan. In 0.60 the accent may be a hex string or an absolute Color; variables, gradients, URLs, and other arbitrary CSS are rejected. DesignSystem also accepts an existing Theme through from_theme() when your organization already owns the token contract.

One brand input, many surfacesDesignSystem.brand(...)
Northstar overviewcomfortable · soft · calm motion
Successful runs42,018↑ 12.4% this week

Preview and audit the result

hedron theme check
hedron theme check --theme aurora --format json
hedron --app app:app style explain --format human
hedron --app app:app style preview --output .artifacts/northstar-gallery --mode all
hedron --app app:app style diff default northstar

style preview writes a deterministic, data-free gallery. It is useful in design review and CI artifacts; it does not execute application callbacks or expose application data.

The canonical 0.60 custom-theme path

Use ThemeBuilder for fluent authoring, but treat the immutable ThemeSpec it produces as the source of truth. Start with the narrowest validation profile that covers the package or app; move to forms, data, workflow, or complete when the theme intentionally covers those surfaces.

from hedron import Hedron, ThemeBuilder, validate_theme_spec

spec = (
    ThemeBuilder("northstar")
    .tokens(
        {
            "color.bg": "#ffffff",
            "color.fg": "#111827",
            "color.muted": "#4b5563",
            "color.border": "#d1d5db",
            "color.accent": "#2563eb",
            "color.focus": "#1d4ed8",
            "color.danger": "#b91c1c",
            "font.family": "system-ui",
            "font.size": "1rem",
            "space.unit": "0.25rem",
            "motion.duration": "160ms",
            "focus.ring": "3px solid #1d4ed8",
        }
    )
    .mode("dark", **{"color.bg": "#111827", "color.fg": "#f9fafb"})
    .accessibility_mode("forced-colors", {"color.focus": "Highlight"})
    .profile("core")
    .build()
)

report = validate_theme_spec(spec, profile="core")
if not report.ok:
    raise RuntimeError(report.to_dict())

app = Hedron(title="Northstar", theme=spec.to_theme(), session_secret="replace-in-production")

Color accepts safe absolute CSS colors such as Color.oklch(0.68, 0.18, 275) and records a deterministic sRGB fallback. It does not accept variables, gradients, URLs, relative colors, or arbitrary CSS. ThemeSpec is immutable and serializable, so fingerprints, reports, and generated assets can be reproduced in CI.

For a reviewed variation, apply an explicit, bounded patch. The base fingerprint prevents a patch from silently landing on the wrong theme revision:

from hedron import Color, ThemePatch

review_patch = ThemePatch(
    name="review-accent",
    base=spec.name,
    base_fingerprint=spec.fingerprint,
    tokens={"color.accent": Color.oklch(0.62, 0.16, 285).to_hex()},
)
review_spec = spec.apply_patches(review_patch)

To distribute a theme, use the data-only package format. Packages contain no executable hooks, remote assets, or runtime CSS injection:

from hedron import conformance_report, load_theme_package, package_theme

package = package_theme(spec, profile="core", licenses=("MIT",))
loaded = load_theme_package(package)
assert conformance_report(loaded, profile="core")["ok"]

The equivalent file-oriented workflow is:

hedron style init --name northstar --output themes/northstar.json
hedron style conform --spec themes/northstar.json --profile core
hedron style package --spec themes/northstar.json --profile core \
  --license MIT --output dist/northstar.hdt

8. Apply semantic style recipes

Recipes package a meaningful combination of presentation props. They are family-scoped, immutable, and conservative: an explicit prop on the component always wins.

Recipe resolutionbuiltin → custom → explicit prop
Recipe defaults
Create pipeline
Explicit prop wins
Create pipeline
from hedron import Button, DesignSystem, StyleRecipe, Surface

primary = StyleRecipe.control(
    "team_primary",
    appearance="solid",
    emphasis="primary",
    size="md",
)
panel = StyleRecipe.surface(
    "team_panel",
    appearance="raised",
    padding="md",
    elevation="sm",
)

design = DesignSystem.brand(
    "team",
    accent="#087f75",
    recipes=(primary, panel),
)

create = design.apply("team_primary", Button("Create pipeline"))
explicit_outline = design.apply(
    "team_primary",
    Button("Create pipeline", appearance="outline"),  # explicit wins
)
panel_view = design.apply("team_panel", Surface("Recent runs"))

The built-in catalog includes recipes such as primary_action, secondary_action, destructive_action, form_surface, dashboard_panel, dense_data, inline_status, and metadata. Custom recipes stay within one family: control, surface, data, status, or content.

9. Scope a theme, mode, or density to a subtree

StyleScope is deliberately small. It marks a subtree with a registered theme, a color mode, and/or a density. It does not create hidden descendant recipe defaults.

Two visual contexts on one pageStyleScope(...)
default · light
Operations

Comfortable density for a primary workspace.

Ready
aurora · dark · compact
Preview

Compact dark context for an embedded surface.

Preview
from hedron import StyleScope, Text

StyleScope(
    Text("Embedded preview", role="title"),
    Text("Uses the aurora dark palette at compact density."),
    theme="aurora",
    color_mode="dark",
    density="compact",
)

Use the smallest meaningful boundary. A page-level theme belongs on Hedron; a preview, embedded report, or mounted surface may justify a StyleScope.

10. Use the 0.60 product-surface contracts

0.60 finishes four commonly hand-styled surfaces. They expose finite presentation props and framework-owned fallback behavior, so they remain legible in print, forced colors, reduced motion, narrow layouts, and no-JavaScript rendering.

from hedron import (
    Brand,
    ConnectorFlow,
    ConnectorNode,
    ConnectorTrack,
    ScrollRegion,
    ToastHost,
)

brand = Brand(
    "Northstar",
    href="/",
    subtitle="Operations workspace",
    subtitle_overflow="break",
)

toast_host = ToastHost(
    placement="bottom-end",
    position="fixed",
    width="field",
    max_width="md",
    gap="sm",
)

flow = ConnectorFlow(
    ConnectorNode("Source", kind="source", state="ready"),
    ConnectorTrack(label="Transfer"),
    ConnectorNode("Warehouse", kind="target", state="running"),
    appearance="soft",
    background="dots",
    overflow="auto",
    min_size="md",
)

events = ScrollRegion(
    "Recent events",
    axis="block",
    size="md",
    affordance="always",
    label="Recent events",
)

Keep Brand and ToastHost in the document shell so fragment swaps do not remove product identity or the reserved #hedron-toast target. Keep ConnectorFlow children in reading order; its visual direction and collapse behavior must never be the only way to understand the workflow. Use ScrollRegion to bound overflow without replacing the semantics of a list, table, timeline, or log. See the focused component pages for the complete finite prop sets: Brand, ToastHost, ConnectorFlow, and ScrollRegion.

Server-first theme preferences

ThemePreference is an allowlisted value, not a user-provided CSS string. Resolve it at the server boundary, render ThemePicker as a native form, and let the application own persistence, authorization, and the POST route. The optional boot helper only reads bounded local values to reduce a preference flash; it does not replace server-rendered state.

from hedron import ThemePicker, ThemePreference, resolve_theme_preference

preference = resolve_theme_preference(
    request.cookies.get("hedron-theme"),
    request.cookies.get("hedron-color-mode"),
    allowed_themes=("default", "aurora"),
)

picker = ThemePicker(
    themes=("default", "aurora"),
    selected=preference,
    action="/preferences/theme",
    csrf_token=csrf_token,
)

For custom hosts, pass the resolved preference through page asset injection so the framework emits theme and color-mode markers before page content. Invalid or stale values fall back to the allowlisted default; never construct a selector, CSS declaration, or persistence key from raw request input.

11. Add CSS only where the component owns the detail

For a custom component, colocate styles.css beside the component and use the documented style-symbol binding. Hedron rewrites local classes and keyframes to collision-free identifiers; :global(...) is explicit when you intentionally target the host.

Scoped component detailCallout/styles.css
Build completed

The visual detail belongs to this callout component, so its rail and tint live with the component.

components/Callout/
├── component.py
└── styles.css
components/Callout/component.py
from collections.abc import Mapping

from hedron_core import NodeLike, StyleSymbols, html, styles_from_manifest


def render_callout(symbols: Mapping[str, str]) -> NodeLike:
    styles: StyleSymbols = styles_from_manifest(symbols, component_id="app:Callout")
    return html.div("Build completed", class_=styles.root)
components/Callout/styles.css
.root {
  border-left: 4px solid var(--color-accent);
  padding: var(--space-md);
}

Maintained complete component reference: Full code on GitHub

For a complete plugin component, register the stylesheet through the plugin manifest; see plugin authoring and themes and scoped styles. Do not copy the docs gallery CSS into the application.

Density is a first-class mode

Density changes rhythm, not meaning. Use it for a whole workspace or a contained data surface, and keep labels and controls understandable at every setting.

Density comparisoncompact · comfortable · spacious
CompactDense tables and tooling.
ComfortableBalanced default rhythm.
SpaciousFocus and presentation.

A production styling checklist

Use this sequence when a screen is ready for review:

  1. Give the page a semantic structure: landmarks, heading levels, labels, and DOM order.
  2. Use shared components and presentation props before adding a class.
  3. Replace repeated prop combinations with a named StyleRecipe.
  4. Set the brand through DesignSystem.brand() or a registered Theme.
  5. Test both light and dark modes, at least one compact/spacious context, and narrow widths.
  6. Preserve focus visibility, text contrast, readable overflow, and reduced-motion behavior.
  7. Run hedron theme check and, for a zero-application-CSS surface, hedron style check --zero-app-css PATH.
  8. Render one full page and one HTMX fragment so swapped regions retain the same styling contract.

What styling does not do

Styling does not authorize an action, validate input, protect CSRF, or make an inaccessible DOM accessible. Keep security and interaction boundaries in the server-side component and action APIs. See accessibility, security, and testing.

Reference map