Skip to content
Version 0.22.0 is out. Twelve CSS modules, one optional script. See what changed
PolyFish CSS

Version 0.22.0, no dependencies

Take the header. Leave the footer.

PolyFish is an HTML and CSS framework split into modules you load individually — nine core, three optional. Building a docs page with no hero and no pricing table? Then you never download the styles for one. Drop the files in, write plain HTML, ship.

  • No build step
  • No required JavaScript
  • MIT licensed
index.htmlpick your modules
<!-- required -->
<link rel="stylesheet" href="PolyFishCSS-Core.css">

<!-- everything below is optional -->
<link rel="stylesheet" href="PolyFishCSS-Headers.css">
<link rel="stylesheet" href="PolyFishCSS-Content.css">
<link rel="stylesheet" href="PolyFishCSS-Footers.css">
<link rel="stylesheet" href="PolyFishCSS-Utilities.css">

In an app? npm i polyfishcss — same files, importable per module, and the optional script is a typed ES module. Bundlers and frameworks.

  • 12CSS files, loaded independently
  • 99 KBNine core modules, one file
  • 20 KBCore alone, the only required file
  • 0Scripts required to render

The short version

What you are actually downloading

Every figure here is produced by the build, not written by hand. The checker fails if the class count drifts and the browser suite fails if the contrast does.

  • 18 KB The whole framework, gzipped. A marketing page loads about 13 KB of it.
  • 0 Build steps, dependencies and required scripts. Link the files and write HTML.
  • 7:1 Minimum contrast for text, links and buttons, measured in a browser on every palette.
  • 0.00 Cumulative layout shift on a throttled phone, with the fonts arriving late.

Why this one

Built the opposite way round

Most frameworks start from a toolchain and add components. This one starts from the page and removes everything that needed a toolchain.

You read the source

Twelve files of plain, commented CSS. No compiler output, no generated class soup, nothing minified beyond recognition. When something looks wrong you open the file and read the rule that did it.

Your CSS wins

Every module sits in a cascade layer, so your own unlayered rules beat the framework without a single !important. There is a browser test that proves it on every run.

A theme is one declaration

Set --pf-accent and the hover, wash, border and focus colours derive from it. Eleven themes ship, each under forty lines, and not one of them forks a component.

The limits are published

Each component documents what it deliberately cannot do, and why. You find the constraint on this site rather than three weeks into your build.

The modules

Every file does one part of the page

Core carries the tokens, the reset and the type scale, so it always ships. Eight more modules map to the parts of a page you either have or don't — these four cover most sites — and three optional files add fonts, state variants and classless styling.

PolyFishCSS-Core.css

Design tokens, reset, type scale, layout primitives, buttons, pills and focus states. Required — the other files read its custom properties.

  • Container, grid, stack, cluster, split
  • Four button variants, three sizes
  • Light-mode flip with .pf-invert

11 KB · required

PolyFishCSS-Headers.css

Everything above the fold: announcement bar, sticky navigation with a working mobile menu, four hero variants, interior page headers, breadcrumbs.

  • Mobile menu runs on a checkbox, not a script
  • Split, centred, compact and photo heroes
  • Backdrop blur with a solid fallback

7 KB · optional

PolyFishCSS-Content.css

The middle of the page. Sections, cards, feature and pricing grids, stats, tables, alerts, forms, accordions, code blocks, galleries, CTA bands.

  • Accordion is a native <details> element
  • Form controls styled down to the select arrow
  • Tables scroll horizontally instead of squashing

12 KB · optional

PolyFishCSS-Footers.css

Column footer, slim footer, green footer. Brand block, link columns, inline signup, social row, legal bar and a back-to-top link.

  • Columns collapse in one step, not four
  • Signup form reuses the Content field styles
  • Green variant recolours itself in full

4 KB · optional

PolyFishCSS-Utilities.css

Spacing, alignment, colour and visibility helpers, all reading the same tokens. The first file to drop if your project already has its own.

  • Margins and gaps on the 4px scale
  • Print rules that hide the chrome
  • Two responsive visibility classes, not twenty

4 KB · optional

Prefer one file?

PolyFishCSS.css is all five concatenated in load order. Use it while prototyping, then swap in the two or three you actually referenced.

Install

Three steps, none of them npm

  1. Copy the files you need

    Drop them next to your HTML. Core first — the rest of the files read tokens that Core defines, so load order matters.

  2. Link the fonts

    Bricolage Grotesque for display, IBM Plex Sans for body, IBM Plex Mono for code. Self-host them or use the link tag; Core falls back to system sans if neither is present.

  3. Write ordinary HTML

    Class names are prefixed pf- so nothing collides with what you already have. No wrapper divs are required beyond a container.

page skeletonhtml
<body>
  <header class="pf-nav">
    <div class="pf-container pf-nav__inner">…</div>
  </header>

  <section class="pf-hero pf-hero--split">…</section>

  <section class="pf-section">
    <div class="pf-container">
      <div class="pf-grid pf-grid--2">…</div>
    </div>
  </section>

  <footer class="pf-footer">…</footer>
</body>

Load order

Core, then Headers, Content and Footers in any order, then Utilities last so its helpers win a specificity tie.

The shape of it

What you actually ship

Measured from the files in this release, not estimated. Every figure here is checked by the build, so it cannot drift from what you download.

Typical page loads. Past six files the bundle is smaller and one request.
Page Files Minified Over the wire Requests
Sign-in screenCore, Content39 KB9.3 KB2
Article or blog post+ Headers, Footers54 KB13.1 KB4
Documentation page+ Space56 KB13.7 KB5
Application screen+ Forms, Components82 KB19.3 KB7
Everything, as the bundleAll nine core99 KB18.2 KB1

0 dependencies

No preprocessor, no plugin ecosystem, no lockfile. The CSS you read in the repository is the CSS the browser gets.

0 required scripts

The menu is a checkbox, tabs are radio inputs, the accordion is <details>. Dialogs are the one exception, and they say so. This site runs an analytics tag; the framework you download does not.

690 classes

Every one of them documented, because the build fails if a class exists that no page on this site mentions.

AAA contrast

Every text-on-surface pair in the default theme, dark and light, measured in a browser on every palette change rather than checked once.

How it works

Two layers of tokens, and that is the whole trick

Almost everything else in the framework follows from this one decision.

  1. A palette names colour

    A neutral ramp from --pf-base-50 to --pf-base-950, plus two accent hues. Named by position, never by hue — a token called --pf-blue-800 is correct in one palette and lying in every other.

  2. Roles name jobs

    Eleven of them: background, deep background, surface, raised surface, text, muted text, accent, second accent, border, strong border, ink. Each points at a palette value.

  3. Components only ever read roles

    No component mentions a colour. That is why a theme is a stylesheet rather than a fork, why light mode touches no component rule, and why the playgrounds on this site can retint a live page from radio inputs.

my-theme.cssthe whole file
:root {
  --pf-bg: #0d1117;
  --pf-bg-deep: #010409;
  --pf-surface: rgba(255, 255, 255, .04);
  --pf-text: #e6edf3;
  --pf-text-muted: #8b949e;
  --pf-accent: #7dd3fc;
  --pf-ink: #04121f;
  --pf-border: rgba(230, 237, 243, .12);
}

And your CSS always wins

Every module sits in a cascade layer, so unlayered author styles override any framework rule with a plain class selector — no !important, and no dependence on which file loaded last. How that works.

Theming

Repaint the whole thing from eleven properties

Components never reference the palette directly. They reference roles — background, surface, text, accent, border — so overriding the roles in one block changes every module at once.

your-theme.csscss
/* loads after Core, overrides the roles */
:root {
  --pf-bg: #000823;
  --pf-bg-deep: #00030f;
  --pf-accent: #8fe06a;
  --pf-accent-2: #e5b189;
  --pf-text: #e9eefa;
  --pf-text-muted: #a1b2cf;
  --pf-radius-lg: 22px;
  --pf-cut: 18px;
}

The cut corner is a variable

Angular surfaces come from one clipped corner, sized by --pf-cut. Set it to 0 and every facet surface becomes a plain rectangle without touching a component rule.

Light islands

Add .pf-invert to any wrapper — this card, a section, the whole body — and the roles flip to a light palette locally. Nested components follow along.

Where it fits

What PolyFish does and doesn't compete with

Feature PolyFish Utility-first frameworks Component kits
Build step None Compiler required Usually bundled
JavaScript None None Often required
Partial loading Per file, by page region By purge output All or nothing
Markup noise One or two classes per element Long class strings Low
Theming Custom properties at runtime Config file, rebuild Sass variables, rebuild
Best for Marketing pages, docs, dashboards App UIs with a toolchain Teams wanting behaviour included

Out of scope, on purpose

PolyFish ships no carousel and no script-driven widgets. Dialogs, menus and accordions open from HTML alone through the platform's own popover, invoker commands and <details>; tabs and the mobile menu run on radio buttons and a checkbox. The optional polyfish.js only adds what markup cannot express — upgrading tabs to a full ARIA tablist — and nothing depends on it.

Eleven palettes

Same markup, different weather

Every palette below renders the identical page — byte for byte the same HTML, with one stylesheet swapped. Open any of them and read the source.

Default

Neutral ground, teal primary, purple second.

Ocean

Blue-green with cyan and indigo, for dashboards.

Ember

Warm near-black with amber and rose.

Sudo

The SudoSecurity house palette — the look this site wears.

See all eleven palettes · Built with PolyFish

Questions

Before you download anything

Do I have to load every module?

Only Core. Every other file assumes Core's custom properties exist and otherwise stands alone — a docs page that loads Core and Content, and nothing else, renders correctly.

Does it work without the web fonts?

Yes. The display, body and mono stacks each fall back to a system face. The type scale, spacing and layout are unchanged; only the letterforms differ.

Why is the mobile menu a checkbox?

So the framework has no script to load, no init call to forget and nothing to break when your bundler moves. The checkbox sits before the menu in the markup and a sibling selector opens it. The label is keyboard focusable and the state is real.

Can I use it on a light background?

The default palette is a near-black neutral because that is what the framework was designed against. Add .pf-invert to the body — or to one section — for a light palette that reuses the same accents.

Is there a Sass source?

No. The files are hand-written CSS with custom properties, which is the point: what you download is what you read, and you can edit it in place without a compiler.

Honest comparison

When to use something else

PolyFish is narrow on purpose. These are the cases where another tool is the better answer, said plainly rather than left for you to find out.

Use a utility-first framework

If your team composes layouts from atomic classes in markup and wants every spacing value available inline, Tailwind does that better and has the toolchain to purge what you do not use. PolyFish gives you components plus a utility layer, not the other way round.

Use a component library

If you need a combobox, a date picker, a virtualised table or a drag-and-drop list, you need JavaScript components. PolyFish deliberately ships none of those, because each only works when a script runs — which would break its one promise.

Use nothing at all

A single landing page with six elements does not need a framework. Take the tokens block from the tokens page, write forty lines of your own CSS, and you will be happier than with any dependency.

What the three approaches actually differ on.
Feature PolyFish Utility-first Component library
Build stepNoneCompiler requiredBundler required
Ships JavaScriptOptional, 8 KBNoneRequired, usually large
Unused CSSLoad only the modules you needPurged at buildTree-shaken at build
RetintingEleven propertiesConfig file, rebuildTheme object, rebuild
Rich widgetsNo combobox, date picker or virtual tableNot its job eitherYes, that is the point
Markup verbosityComponent classes, shortLong utility stringsFramework components
Interactive widgetsPlatform elements — dialog, details, invoker commandsNot its jobJavaScript reimplementations
Right-to-leftLogical properties throughoutPer-utility variantsVaries by library
Print stylesUtilities plus print rules, no buildPer-utility print: variantRarely shipped
Works in a Ghost or WordPress themeDrop in two link tagsNeeds a build in the themeUsually impractical

Where it came from

Built for a publication, in public

PolyFish is the front end of SudoSecurity, a technical publication covering security tooling and developer releases. Every constraint in it came from shipping that: no runtime because a static site should not need one, modules because an article page has no pricing table, a measure token because a 3,000-word piece has to stay readable on a phone at midnight.

The framework is developed against the hardest case it has — long-form technical writing with code, tables of versions and CVE references, and readers who will open it on anything. A marketing page is the easy case, and frameworks built only for the easy case show it.

More about the decisions and the failures behind them.

Verified, not asserted

Thirteen groups of checks run against every page and stylesheet on every build: structure, links, policy, asset hashes, palette agreement, module coverage, documentation coverage, mobile, accessibility and CSS integrity. The build fails rather than warns.

They have caught a redirect loop, a class declared in two modules, a grid track that collapsed text to one character per line, and a caching rule that served a stale palette to returning visitors.

What they cannot catch

Anything that only exists once a browser lays the page out. That is why the repository carries a Playwright suite across five device profiles, and why the version number stays below 1.0 until it runs green.

Twelve files. Start with one.

Take Core, add whatever the page actually has, and delete the rest from your project folder. Or skip the assembly: the starters are four complete pages you can overwrite.

Start with one file

Core is the only required module. Add the rest as the page needs them, and delete the ones it does not.