Skip to content
PolyFish CSS

Build a page, region by region

Swap the navigation, the hero, the body and the footer, then change the tokens underneath them. Every control below is a radio input and a :has() rule — there is no script on this page doing any of it.

Header
Hero
Body
Footer
Theme
Accent
--pf-cut
Radius
Rhythm
Regions
Announcement bar, for the one thing that matters.
Northbeam
Start free

Split hero

Copy on the left, evidence on the right.

The variant for a product with something to show.

PrimarySecondary
config.ymlyaml
region: eu-west
every: 30s

Centred hero

One column, nothing to prove yet.

For launches and anything without a screenshot worth showing.

PrimarySecondary

Interior page header

Shorter, because the visitor has already arrived.

Card grid

Three across, one down on a phone

Facet

Clipped corner, sized by the token above.

Surface

Hairline border over the page colour.

Accent

Wash and a stronger border.

Split body

Prose beside a panel

  • Collapses to one column under 420px of track
  • Alignment is centred by default
  • Gap follows --pf-gap-lg

Alert

Colour carried by a rule and a wash.

BadgePillChip

Data

A table that scrolls rather than squashes

CheckStatusp95
api-gatewayHealthy184ms
billing-workerDegraded1.8s
auth-serviceHealthy96ms

Header .pf-nav.pf-topbar + .pf-nav.pf-nav--flush · Hero .pf-hero--split.pf-hero--center.pf-pagehead · Footer .pf-footer.pf-footer--slim.pf-footer--accent

How the region swap works

CSS cannot change a class, so every variant is in the markup and hidden. A :has() rule turns the selected one back on. For a docs page showing three navigation bars that is the right trade; on a real page you would render the one you chose.

How the token swap works

Theme, accent, cut, radius and rhythm each redeclare one or two custom properties on the stage. No component rule is touched, and nothing is reclassed — which is the whole argument for the token layer, demonstrated rather than described.

Zero JavaScript

Radio inputs, a checkbox and :has(). Where :has() is unsupported the stage shows its defaults and the controls do nothing — a degradation, not a break.

Other things you can poke

Every documentation page on this site has something interactive in it: swap page regions, retint a live page, change the type scale, see what each module unlocks, assemble your link block, frame a real page at 320px, weigh a page type, build an attribute list, turn the invisible guards on, flip a component through its states and filter the changelog by tag. All of it is radio inputs, checkboxes and :has() — no script runs any of it.