framework/ui on framework/headless

One page, five themes, zero bespoke CSS

Every control below reads its look from the route's registered theme: palette tokens for colour, component options for density, treatment and radius. Same markup, same classes, different theme boundary.

A generated dashboard mockup standing in for a product shot, drawn through the framework's image pipeline.

The page under the theme

Long-form content, headings and links, all reading theme tokens.

This page is the living proof for two documents: headless components own structure, and theming owns the look. Nothing between the hero and this sentence carries a site-authored style; every value resolves through a theme variable.

Why two layers

A component's structure — its tags, roles, label wiring — changes with the design system's semantics, not with its palette. Splitting the two means a re-skin is a theme edit, and an accessibility fix never re-opens a colour decision. The Button rebuild is the first component to ship on that split.

What the fixtures below prove

  • Options, not palette. The "same palette" fixture changes only the option set; the colours are the page's own.
  • Nesting by inheritance. Theme boundaries declare the option variables, component rules consume them, so A → B → A ends on A without a single descendant rule.
  • Demand loading. The late fragment's stylesheet is not on the page until the runtime sees its marker.

A form with a real round trip

Server-validated. With the runtime: an island swap and focus lands on the summary. Without script: the same POST redirects back and this page re-renders the answer.

One field, server-side validation, island round trip with script, plain POST without.

Field layouts under constraint

The dense route lays labels out inline; below the sheet's width, in a narrow container as much as a narrow viewport, the rows stack instead of overflowing.

A label this long wraps rather than widening its track.

The rule: 1–65535.

A field whose control is an input group.

A whole field inside a group.

Every variant, every size

Variant is what a button means; treatment is how the theme draws it. One disabled, one with an icon, one external link.

Primary

Secondary

Danger

Ghost

Same palette, two option sets

The right panel is a nested ui.Themed whose theme keeps this page's tokens and changes only the options. Colour stays; density, treatment and radius flip.

This page's theme

Density, treatment and radius as registered for the route.

The other option set

Same palette, the flipped option set: the option compiler's work isolated from the tokens.

Nesting A → B → A

Three primary buttons. Each boundary declares the full option set; read the computed values and the innermost A matches the outer A.

Dense

The page's theme: the outer A.

Framework default

The other registered theme, nested inside the page's scope.

Dense again

The innermost scope redeclares the complete option set, so it wins by proximity: A → B → A ends on A.

Light, dark, system

The pill forces a scheme independent of the OS setting; Auto follows it. Scoped dark palettes flip with the document, not the wrapper.

Bare headless, styled ui

The left button is headless.Button with a nil Classes: structure, roles, no classes, no stylesheet. The right one is ui.Button on the same page. The banner beneath is headless.SystemBanner with its Strings resolved from the request through ui.StringsFor — English words here, the reader's locale the moment the site installs a translator.

headless.Button, nil Classes

ui.Button

headless.SystemBanner, ui.StringsFor(ctx)

Information: Strings from the request

The tone word and the dismiss name are headless.Strings fields; ui.StringsFor resolves them per request.

A cold LoadAuto insertion

Button is LoadAlways and proves nothing about demand loading. The fragment below arrives with a ui.Callout, whose sheet is LoadAuto and absent here: the runtime must fetch it when the marker appears.

Nothing loaded yet. The fragment's component stylesheet is not on this page.