Skip to content
PolyFish CSS

Tokens

Every value in the framework is a custom property declared on :root in Core. Components reference roles, never the palette, which is what makes a reskin an eleven-line change.

Palette

Raw values. Change these to shift the whole brand, or leave them and remap the roles below.

TokenValueUsed for
--pf-base-900#00030fDeep surfaces: footers, code blocks, alt sections
--pf-base-800#000823Page background
--pf-base-700#000f3fRaised surface — the lightest background in the framework
--pf-base-600#17306eInfo alert rule
--pf-hue-1#8fe06aAccent: buttons, links, markers
--pf-hue-1-deep#6dc44aAccent hover and press
--pf-hue-1-washrgba(143, 224, 106,.12)Accent tint behind icons and cards
--pf-hue-2#e5b189Secondary accent
--pf-hue-2-deep#c48f66Secondary hover
--pf-base-50#e9eefaPrimary text
--pf-base-300#a9bcd8Muted text
--pf-base-950#00081fText on accent fills

Roles

These are what components actually reference. Override these eleven and every module follows.

TokenDefaultMeaning
--pf-bgnavy-800Page background
--pf-bg-deepnavy-900Recessed background
--pf-surfacewhite 4.5%Cards, alerts, plan cards
--pf-surface-raisedwhite 8%Hover state for surfaces
--pf-textpaperBody text
--pf-text-mutedmistSecondary text, help text
--pf-accentgreenPrimary actions and links
--pf-accent-2orangeSecondary emphasis
--pf-bordermist 22%Hairline borders
--pf-border-stronggreen 42%Emphasised borders
--pf-focusgreenFocus ring colour

Readability

Six decisions, each of which can be overridden, and each of which was made in the same direction: toward the reader.

DecisionWhy
Base is 17–19px, fluidThe 16px default is inherited from 1996 monitors, not from a reading study. Body copy grows with the viewport rather than staying fixed.
Line height 1.7, prose 1.75The largest readability lever after size. Tightens as type grows, because a 60px headline at 1.7 leaves its lines unrelated.
66-character measureThe comfortable range is roughly 45–75. Past that, the return sweep to the next line starts landing in the wrong place.
Nothing below 13.6pxThe old 12px step is a decision to have the text not read. The smallest size is now 0.8125rem.
Muted text at 6.79:1It was 5.49:1 — passing AA and still the first thing people squint at. Secondary text is still text.
Links underlined by defaultA link identified only by colour is invisible to anyone who cannot separate that colour from the text around it. text-decoration-skip-ink keeps the underline out of the descenders.

Headings also carry more space above than below, because a heading belongs to what follows it. Without that asymmetry a page reads as a list of fragments rather than as sections.

Type

Three stacks and a nine-step scale. The top four steps are fluid, clamped between a phone and a desktop width.

TokenValue
--pf-font-displayBricolage Grotesque, Trebuchet MS, system-ui
--pf-font-bodyInter, system-ui, -apple-system, Segoe UI, Roboto
--pf-font-monoIBM Plex Mono, ui-monospace, Menlo
--pf-text-xs … -base0.8125, 0.9375, 1rem — and the base itself is fluid, 17px to 19px
--pf-text-lg … -2xl1.1875, 1.5, 1.9375rem
--pf-text-3xl … -5xlfluid: 2.25→3, 2.75→4.4, 3.25→6rem
--pf-leading1.7 body, 1.75 prose, 1.22 snug, 1.06 display
--pf-measure66ch, applied to p and li

Try the scale

Three controls, six token overrides, no reclassing. The specimen below is ordinary markup — a heading, a lede, body copy and a card — reading whatever the stage declares.

type scale
--pf-measure
--pf-leading

Specimen

Large headers, generous spacing

The lede reads --pf-text-lg and the measure token, so it narrows and widens with the controls above.

Body copy sits at --pf-text-base and is capped at --pf-measure. That cap is the one thing the framework imposes on your content, and it is the difference between a readable column and a line the eye cannot track back to. Sixty-eight characters is the default; narrower suits a sidebar, wider suits a reference page where tables want the room.

Card title

Card copy reads --pf-text-sm, so it moves with the scale but stays a step below the body.

--pf-text-xs --pf-text-sm --pf-text-base --pf-text-lg --pf-text-xl --pf-text-2xl --pf-text-3xl

Radio inputs and :has(). The stage declares tokens; every component inside reads them.

Space and shape

Nine space steps from 4px, doubling at the top. Radii are deliberately few — angles do the distinctive work through --pf-cut.

TokenValue
--pf-space-1 … -9.25, .5, .75, 1, 1.5, 2, 3, 4.5, 7rem
--pf-gapclamp(1.25rem, 2.5vw, 2rem) — grid and stat gaps
--pf-gap-lgclamp(2rem, 4vw, 3.5rem) — splits, shells, section heads
--pf-section-padclamp(4rem, 9vw, 7.5rem) — vertical rhythm between sections
--pf-radius-sm / / -lg6px, 12px, 22px
--pf-radius-pill999px
--pf-cut18px — depth of the clipped corner on facet surfaces
--pf-ring3px accent glow, used on focused inputs
--pf-shadow-smHairline depth, for raised rows
--pf-shadowMenus, popovers, hover lift
--pf-liftDialogs and hero media

Layout and motion

TokenValue
--pf-container1160px; --narrow is 780px, --wide 1360px
--pf-gutterclamp(1.25rem, 5vw, 3rem)
--pf-speed160ms
--pf-easecubic-bezier(.2,.7,.3,1)
--pf-meshThe triangle pattern, as an inline SVG data URI
--pf-minPer-grid override for .pf-grid--auto, default 260px

Renamed in 0.16 and 0.17

Palette tokens were named after hues, which is correct in one palette and wrong in every other: --pf-blue-800 held #f4f7fa in the cream theme and --pf-green held #ccff00 in the electric one. They now name position on a scale instead. Load PolyFishCSS-Compat.min.css after Core and your existing markup keeps working while you migrate.

WasNowNote
--pf-blue-900--pf-base-900Deepest surface
--pf-blue-800--pf-base-800Page background
--pf-blue-700--pf-base-700Raised surface
--pf-blue-600--pf-base-600Emphasis border
--pf-paper--pf-base-50Lightest tone
--pf-mist--pf-base-300Muted tone
--pf-ink--pf-base-950Text on an accent fill
--pf-green, -deep, -wash--pf-hue-1, -deep, -washPrimary accent hue
--pf-orange, -deep, -wash--pf-hue-2, -deep, -washSecond accent hue
.pf-fg-green.pf-fg-accentHelpers now name the role, not the colour
.pf-fg-orange.pf-fg-accent-2
.pf-bg-green-wash.pf-bg-accent-wash
.pf-bg-orange-wash.pf-bg-accent-2-wash
.pf-bg-blue.pf-bg-base
.pf-bordered-green.pf-bordered-accent
.pf-btn--orange.pf-btn--accent-2
.pf-pill--green, .pf-pill--orange.pf-pill--accent, --accent-2
.pf-mark--orange.pf-mark--accent-2
.pf-card__icon--orange.pf-card__icon--accent-2
.pf-checks--orange.pf-checks--accent-2
.pf-topbar--green.pf-topbar--accent
.pf-footer--green.pf-footer--accent
pf-ml-0 pf-ml-px pf-ml-1 pf-ml-2 pf-ml-3 pf-ml-4 pf-ml-5 pf-ml-6 pf-ml-7 pf-ml-8 pf-ml-9 pf-ml-auto.pf-ms-*Logical spacing. The old names implied a physical side while applying a logical one
pf-mr-0 pf-mr-px pf-mr-1 pf-mr-2 pf-mr-3 pf-mr-4 pf-mr-5 pf-mr-6 pf-mr-7 pf-mr-8 pf-mr-9 pf-mr-auto.pf-me-*
pf-pl-0 pf-pl-px pf-pl-1 pf-pl-2 pf-pl-3 pf-pl-4 pf-pl-5 pf-pl-6 pf-pl-7 pf-pl-8 pf-pl-9.pf-ps-*
pf-pr-0 pf-pr-px pf-pr-1 pf-pr-2 pf-pr-3 pf-pr-4 pf-pr-5 pf-pr-6 pf-pr-7 pf-pr-8 pf-pr-9.pf-pe-*
.pf-text-left, .pf-text-right.pf-text-start, .pf-text-endSame reason

What the shim cannot do

It lets old names read new values. It does not let old declarations reach new components: if you retinted the framework by declaring --pf-green yourself, that declaration no longer feeds anything, and you need to rename it to --pf-hue-1. One line, and the shim is 1 KB you can then delete.

Light and dark

The framework ships dark and can follow the operating system. Only the palette is redeclared; every role, and therefore every component, follows.

MarkupBehaviour
<html lang="en">Follows prefers-color-scheme
data-pf-theme="dark"Always dark, whatever the OS says
data-pf-theme="light"Always light

The accent darkens in light mode rather than staying put. A pastel green that reads at 6.7:1 on deep blue reads at 1.6:1 on near-white — a palette that does not change with its background is decoration, not a palette. This site pins itself to dark because the dark page is its identity.

Adapting to the device

Six preference and capability queries are handled in Core, so a component never has to think about them.

QueryWhat changes
prefers-reduced-motionEvery transition and animation drops to 0.01ms; smooth scrolling is disabled
prefers-contrast: moreHairlines become solid 2px borders; muted text moves to full strength
forced-colors: activeBackgrounds are replaced by system keywords and borders reinstated, so nothing flattens into the page in Windows High Contrast
pointer: coarseInteractive targets get a 44px minimum height; checkboxes grow
Short landscapeSection and hero padding is capped so a 1024×600 screen is not all whitespace
prefers-color-schemeThe whole palette flips to light, unless the page pins a theme
hover: hoverHover affordances are only defined where a pointer can hover
@containerComponents respond to their container's width, not the viewport's
printNavigation, footers and CTA bands are hidden

All motion respects the user

A prefers-reduced-motion block reduces every transition and animation to 0.01ms and disables smooth scrolling. You do not need to handle this yourself.


Next: theming.

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.