Skip to content

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.

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.

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, 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.

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 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.

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. 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.

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/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.

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