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.
Version 0.22.0, no dependencies
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.
<!-- 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.
The short version
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.
Why this one
Most frameworks start from a toolchain and add components. This one starts from the page and removes everything that needed a toolchain.
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.
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.
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.
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
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.
Design tokens, reset, type scale, layout primitives, buttons, pills and focus states. Required — the other files read its custom properties.
.pf-invert11 KB · required
Everything above the fold: announcement bar, sticky navigation with a working mobile menu, four hero variants, interior page headers, breadcrumbs.
7 KB · optional
The middle of the page. Sections, cards, feature and pricing grids, stats, tables, alerts, forms, accordions, code blocks, galleries, CTA bands.
<details> element12 KB · optional
Column footer, slim footer, green footer. Brand block, link columns, inline signup, social row, legal bar and a back-to-top link.
4 KB · optional
Spacing, alignment, colour and visibility helpers, all reading the same tokens. The first file to drop if your project already has its own.
4 KB · optional
PolyFishCSS.css is all five concatenated in load order. Use it while prototyping, then swap in the two or three you actually referenced.
Install
Drop them next to your HTML. Core first — the rest of the files read tokens that Core defines, so load order matters.
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.
Class names are prefixed pf- so nothing collides with what you already have. No wrapper divs are required beyond a container.
<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
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.
| Page | Files | Minified | Over the wire | Requests |
|---|---|---|---|---|
| Sign-in screen | Core, Content | 39 KB | 9.3 KB | 2 |
| Article or blog post | + Headers, Footers | 54 KB | 13.1 KB | 4 |
| Documentation page | + Space | 56 KB | 13.7 KB | 5 |
| Application screen | + Forms, Components | 82 KB | 19.3 KB | 7 |
| Everything, as the bundle | All nine core | 99 KB | 18.2 KB | 1 |
No preprocessor, no plugin ecosystem, no lockfile. The CSS you read in the repository is the CSS the browser gets.
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.
Every one of them documented, because the build fails if a class exists that no page on this site mentions.
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
Almost everything else in the framework follows from this one decision.
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.
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.
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.
: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
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.
/* 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;
}
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.
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
| 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
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.
Questions
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.
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.
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.
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.
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
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.
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.
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.
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.
| Feature | PolyFish | Utility-first | Component library |
|---|---|---|---|
| Build step | None | Compiler required | Bundler required |
| Ships JavaScript | Optional, 8 KB | None | Required, usually large |
| Unused CSS | Load only the modules you need | Purged at build | Tree-shaken at build |
| Retinting | Eleven properties | Config file, rebuild | Theme object, rebuild |
| Rich widgets | No combobox, date picker or virtual table | Not its job either | Yes, that is the point |
| Markup verbosity | Component classes, short | Long utility strings | Framework components |
| Interactive widgets | Platform elements — dialog, details, invoker commands | Not its job | JavaScript reimplementations |
| Right-to-left | Logical properties throughout | Per-utility variants | Varies by library |
| Print styles | Utilities plus print rules, no build | Per-utility print: variant | Rarely shipped |
| Works in a Ghost or WordPress theme | Drop in two link tags | Needs a build in the theme | Usually impractical |
Where it came from
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.
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.
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.
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.
Core is the only required module. Add the rest as the page needs them, and delete the ones it does not.