Presentation · themes · CSS boundaries
Style the whole interface, one vocabulary at a time¶
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.59
The 0.59 styling contract adds compiler format 2, opt-in container-query boundaries, finite theme variants, modern color and typography fallbacks, logical overlay placement, print and preference media, and validated typed-control attributes. The complete capability matrix—including Progressive, Experimental, and Deferred CSS features—is in Modern CSS in 0.59.
The styling stack¶
There are four layers. Start at the top and move down only when the layer above cannot express the intent.
<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.
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.
The most-used values are:
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.
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.
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.
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.
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.
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.
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, typed 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 hex 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. DesignSystem also accepts an existing Theme through from_theme() when your
organization already owns the token contract.
Preview and audit the result¶
hedron theme check
hedron theme check --theme northstar --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 --base default --candidate 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.
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.
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.
default · lightComfortable density for a primary workspace.
Readyaurora · dark · compactCompact dark context for an embedded surface.
Previewfrom 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. Add CSS only where the component owns the detail¶
For a custom component, colocate styles.css beside the component and use the typed
style-symbol binding. Hedron rewrites local classes and keyframes to collision-free
identifiers; :global(...) is explicit when you intentionally target the host.
The visual detail belongs to this callout component, so its rail and tint live with the component.
/* components/Callout/styles.css */
.root {
border-left: 4px solid var(--color-accent);
padding: var(--space-md);
}
from hedron_core import StyleSymbols, styles_from_manifest
styles: StyleSymbols = styles_from_manifest(symbols, component_id="app:Callout")
return html.div("Build completed", class_=styles.root)
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.
A production styling checklist¶
Use this sequence when a screen is ready for review:
- Give the page a semantic structure: landmarks, heading levels, labels, and DOM order.
- Use shared components and presentation props before adding a class.
- Replace repeated prop combinations with a named
StyleRecipe. - Set the brand through
DesignSystem.brand()or a registeredTheme. - Test both light and dark modes, at least one compact/spacious context, and narrow widths.
- Preserve focus visibility, text contrast, readable overflow, and reduced-motion behavior.
- Run
hedron theme checkand, for a zero-application-CSS surface,hedron style check --zero-app-css PATH. - 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¶
- Presentation API — shared vocabulary and progressive styling APIs
- Modern CSS in 0.59 — complete feature tiers and fallbacks
- Themes and scoped styles — themes, CSS layers, and
styles.css - StyleScope — subtree theme/mode/density boundaries
- Component demos — visual pages for every built-in
- CLI reference —
theme check,style explain,style preview, andstyle diff