What it is
PolyFish is five hand-written CSS files covering the parts a page is made of: a required core of tokens and primitives, then headers, content, footers and utilities. You link the ones your page has and skip the ones it doesn't. There is no compiler and no configuration file, and nothing needs JavaScript to render.
What it is not
Behaviour is optional and it is separate. Every component renders and functions from CSS alone; an optional 8 KB script upgrades a handful of them — closing a menu on an outside click, giving the tabs a real ARIA tablist, counting characters in a textarea. Nothing depends on it, and nothing is hidden by it.
It is also not a utility-first framework. Utilities exist in the fifth file and they are deliberately small — enough to stop you writing one-off rules, not enough to build a page out of class strings.
Principles
- Readable source. What you download is what you read. Every file has section headers and comments explaining why a rule exists.
- Roles, not colours. Components reference
--pf-accent, never a hex value. Retheming is eleven custom properties, applied at runtime. - Ordinary HTML. One or two classes per element, no required wrapper divs beyond a container.
- Prefixed everything. Every class starts with
pf-, so dropping it into an existing project collides with nothing.
Versioning
PolyFish follows semantic versioning. Within a major version, class names and token names do not change meaning — a value may be tuned, a component may gain a variant, but existing markup keeps rendering the way it did. Removals wait for a major.
Browser support
The framework uses custom properties, clamp(), grid, gap, clip-path and color-mix(). Everything degrades: where color-mix() is unsupported the sticky navigation falls back to a solid background, and where backdrop-filter is unsupported it simply doesn't blur. Anything from roughly 2023 onward renders as designed.
Licence
MIT. Use it commercially, fork it, rename it, ship it inside a product. Attribution is appreciated and not required.
The name
The mark is a betta built out of a hexagon and three fans of triangles, all radiating from a single point. It is the framework's argument in one image: a small number of regular pieces, arranged deliberately, is enough.
Where the constraints came from
Each rule below exists because of a specific failure, not a preference. They are in the order they were learned.
| Rule | The failure behind it |
|---|---|
| Components read roles, never palette values | Retinting a framework by editing component rules means every update re-opens the same merge conflict. Eleven role properties make a theme a stylesheet instead of a fork. |
| No element is hidden by JavaScript on load | A script that hides content and then fails to load has deleted that content. The optional layer only upgrades what already works. |
| Nothing injects CSS from script | A script that writes styles forces unsafe-inline into your Content-Security-Policy. The two elements the script creates are styled in the Content module instead. |
| Data values get utility classes, not inline styles | A rating bar needing style="width: 69%" is a framework quietly forcing you to weaken your policy. Twenty-one width classes are cheaper than that. |
| Every animation sits inside a reduced-motion guard | Slowing an animation that starts at opacity: 0 to nothing leaves the element invisible forever. Not applying it is the only correct behaviour. |
Grid children carry min-width: 0 | A wide table stretches its track past 1fr and scrolls the whole page sideways. One line, and it was latent here for four versions. |
| Form fields are floored at 16px on touch | iOS Safari zooms on focus below that and never zooms back out. The fields were at 14px. |
Versioning
Inside a major version, class names and token names do not change meaning. A value may be tuned and a component may gain a variant, but existing markup keeps rendering the way it did. Removals wait for a major and are deprecated in a minor first. The changelog records every release, including the ones that only fixed something embarrassing — the redirect loop, the class collision between two modules, the heading levels that broke outline navigation.
How the site verifies itself
Every claim on this site is checked by a script rather than remembered, because the numbers move every release. The build fails on any of these:
- Structure and SEO — one
h1per page, no duplicate ids, every internal link and fragment resolving, canonical uniqueness, sitemap parity, no orphan pages. - Documentation coverage — every public class must appear somewhere on the site. It was 41% before the check existed.
- Module waste — a page may not link a module it never uses. Seventeen pages were shipping one.
- Consistency — every site page uses the shared header, the shared footer, the theme toggle and one page head. Eighteen pages once carried thirteen different navigations.
- Mobile — nineteen checks covering viewport, fixed widths, table wrappers, viewport units, touch targets, iOS zoom, safe areas and menu reachability.
- Accessibility — heading order, landmarks, an accessible name on every control, link text, skip-link targets.
- Correctness — W3C validation on every page, source-versus-minified rule parity on every module, and 69 jsdom assertions on the optional script, twelve of which check that the page survives the script's absence.
The build is also deterministic: it runs four times in a row producing byte-identical output, because a non-deterministic build makes a real change indistinguishable from noise.