Get the files
Download them individually or take the concatenated bundle while prototyping.
| Module | Source | Minified | Min + gzip | Required |
|---|---|---|---|---|
| Core · min | 32.1 KB | 14.2 KB | 4.1 KB | Always |
| Grid · min | 17.6 KB | 12.6 KB | 1.6 KB | 12-column layouts |
| Headers · min | 13.2 KB | 6.7 KB | 1.8 KB | With a nav or hero |
| Content · min | 30.5 KB | 14.7 KB | 3.3 KB | Page body components |
| Forms · min | 12.7 KB | 5.4 KB | 1.5 KB | Beyond the four basic controls |
| Components · min | 35.5 KB | 16.9 KB | 3.5 KB | App UI: badges, tabs, menus, lists |
| Footers · min | 6.0 KB | 3.5 KB | 1.0 KB | With a footer |
| Space · min | 26.9 KB | 19.8 KB | 2.8 KB | Padding, margin and gap utilities |
| Effects · min | 11.8 KB | 6.1 KB | 1.6 KB | Transforms, filters, animations |
| Utilities · min | 16.8 KB | 8.9 KB | 2.3 KB | Optional helpers |
| Fonts · min | 4.8 KB | 2.6 KB | 0.4 KB | Self-hosted @font-face |
| polyfish.js · min | 23.0 KB | 9.6 KB | 3.6 KB | Optional enhancements |
| Bundle · min | 204.0 KB | 108.3 KB | 19.2 KB | Every CSS module except Fonts |
Two builds, same rules
The source files are heavily commented — every component carries its markup
example and the reasoning behind the technique — which is why they are roughly
twice the size of the minified build. Ship .min.css, read the source.
Both were parsed and confirmed to contain an identical number of rules.
Fonts are optional and self-hosted
PolyFishCSS-Fonts.css declares seven @font-face rules pointing at
/fonts, about 152 KB of latin-subset woff2. There is no request to a
font CDN anywhere on this site, which is what lets the Content-Security-Policy
refuse every remote origin outright. Skip the file and the system stacks take over.
Two builds, same rules
The source files are heavily commented — every component carries its
markup example and the reasoning behind the technique — which is why they
are roughly twice the size of the minified build. Ship .min.css;
read the source. Both were parsed and confirmed to contain an identical
number of rules.
Load order
Core first, always. Headers, Content and Footers can appear in any order between them. Utilities goes last so a helper class beats a component rule at equal specificity.
<link rel="stylesheet" href="/css/PolyFishCSS-Core.css">
<link rel="stylesheet" href="/css/PolyFishCSS-Headers.css">
<link rel="stylesheet" href="/css/PolyFishCSS-Content.css">
<link rel="stylesheet" href="/css/PolyFishCSS-Footers.css">
<link rel="stylesheet" href="/css/PolyFishCSS-Utilities.css">
<link rel="stylesheet" href="/css/your-theme.css"> <!-- overrides -->
Core is not optional
The other four files read custom properties that Core defines. Without it they compute to nothing and the page renders unstyled.
Fonts
PolyFish names three families — Bricolage Grotesque for display, IBM Plex Sans for body, IBM Plex Mono for code — and ships them as latin-subset woff2 files you serve yourself. There is no request to a font CDN anywhere in the framework or on this site.
<!-- the two faces above the fold -->
<link rel="preload" as="font" type="font/woff2" crossorigin
href="/fonts/ibm-plex-sans-latin-400-normal.woff2">
<link rel="stylesheet" href="/PolyFishCSS-Fonts.css?v=52fae739">
Copy the /fonts directory alongside the CSS. The
crossorigin attribute on a font preload is not optional — fonts are
fetched in CORS mode, and a preload without it downloads the file twice.
Skip the module entirely if you would rather use system faces. Core's stacks fall back to Trebuchet, system-ui and ui-monospace; the type scale, spacing and layout are unchanged, only the letterforms differ.
Pick your modules
Tick what your page contains. The link block below is what goes in your
<head> — Core is not a checkbox because it is not optional.
<!-- required, 14 KB -->
<link rel="stylesheet" href="/PolyFishCSS-Core.min.css?v=a8a23d99">
<link rel="stylesheet" href="/PolyFishCSS-Grid.min.css?v=1508bc56"> 13 KB<link rel="stylesheet" href="/PolyFishCSS-Headers.min.css?v=45467afc"> 7 KB<link rel="stylesheet" href="/PolyFishCSS-Content.min.css?v=324c0216"> 15 KB<link rel="stylesheet" href="/PolyFishCSS-Forms.min.css?v=597b4a30"> 5 KB<link rel="stylesheet" href="/PolyFishCSS-Components.min.css?v=1780340e"> 17 KB<link rel="stylesheet" href="/PolyFishCSS-Space.min.css?v=5a3c53e8"> 20 KB<link rel="stylesheet" href="/PolyFishCSS-Effects.min.css?v=0b639db9"> 6 KB<link rel="stylesheet" href="/PolyFishCSS-Utilities.min.css?v=3d34518e"> 9 KB
That is eight or more modules — ship PolyFishCSS.min.css instead. It is 19.3 KB gzipped in one request, smaller than the same modules fetched separately.
Checkboxes and :has(). Sizes are per-file minified; the gzipped total is on the table above.
When to use the bundle
Loading only the modules a page uses is the right default — but not past a point, and the point is measurable. Gzip finds far more repetition inside one file than across eight files compressed separately, so the bundle overtakes individual modules at eight:
| Modules loaded | Separate files, gzipped | Requests |
|---|---|---|
| 4 | 10.9 KB | 4 |
| 6 | 15.9 KB | 6 |
| 7 | 16.9 KB | 7 |
| 8 | 19.8 KB | 8 |
| All ten, as the bundle | 19.3 KB | 1 |
So: fewer than eight modules, link them individually. Eight or more, ship
PolyFishCSS.min.css — it is smaller and one request. This site follows its own
rule: the documentation pages use the bundle, the example pages link four to six modules each.
Your CSS always wins
Every module is wrapped in a cascade layer, and each file declares the full layer order, so it holds regardless of which order you link them in. Unlayered CSS beats layered CSS whatever the specificity, which means your own stylesheet overrides any framework rule with a plain class selector.
<link rel="stylesheet" href="/PolyFishCSS-Core.min.css?v=a8a23d99">
<link rel="stylesheet" href="/app.css"> <!-- wins, always -->
No !important, no specificity arms race, and no worrying about
whether Utilities loaded last. If you would rather layer your own CSS too, name a layer after
the framework's:
@layer polyfish, app;
@layer app {
.pf-card { background: rebeccapurple; }
}
Right to left
Nothing in the framework uses a physical direction. Margins, padding, borders, positioning
offsets and text alignment are all logical properties, so a document with
dir="rtl" mirrors itself — no second stylesheet, no overrides, no
[dir] selectors.
<html lang="ar" dir="rtl">
The right-to-left example is the same components
with Arabic copy and that one attribute. A build check fails if a physical property such as
margin-left ever reappears in a module, because that is easy to write by habit
and invisible until somebody reads the site in Arabic or Hebrew.
Two notes. Values that are genuinely left-to-right — an email address, a version number,
a code sample — take dir="ltr" on the element that holds them. And the
directional spacing utilities are .pf-ms-*, .pf-me-*,
.pf-ps-* and .pf-pe-* for start and end; the old
ml/mr/pl/pr names live in the compatibility file.
The optional script
PolyFish renders and functions with no script at all. One optional file upgrades a handful of components — closing a menu on an outside click, giving the tabs a real ARIA tablist, counting characters in a textarea, sorting a table column.
<script src="/polyfish.min.js?v=a39398cb" defer></script>
Nothing changes until you mark an element up with a data-pf-*
attribute. Full list on the JavaScript page.
Folder layout
Nothing is path-dependent. A conventional layout:
/
index.html
about.html
/css/
PolyFishCSS-Core.css
PolyFishCSS-Headers.css
PolyFishCSS-Content.css
PolyFishCSS-Footers.css
PolyFishCSS-Utilities.css
your-theme.css
/examples/
/docs/
Serving it
Any static host works. If you want extensionless URLs — /about rather than /about.html — the server needs to resolve the extension itself. This site's own config is published as a starting point:
location / {
# /about → /about.html, /docs → /docs/index.html
try_files $uri $uri.html $uri/ =404;
}
The complete server block, including the redirect that strips .html from old URLs, is in nginx/polyfish.org.conf.
Next: what's inside each module.