Skip to content
PolyFish CSS

Installation

There is nothing to install. Copy the CSS files into your project, link the ones your page needs, and write HTML. Eleven CSS modules, one of them required, plus an optional script.

Get the files

Download them individually or take the concatenated bundle while prototyping.

ModuleSourceMinifiedMin + gzipRequired
Core · min32.1 KB14.2 KB4.1 KBAlways
Grid · min17.6 KB12.6 KB1.6 KB12-column layouts
Headers · min13.2 KB6.7 KB1.8 KBWith a nav or hero
Content · min30.5 KB14.7 KB3.3 KBPage body components
Forms · min12.7 KB5.4 KB1.5 KBBeyond the four basic controls
Components · min35.5 KB16.9 KB3.5 KBApp UI: badges, tabs, menus, lists
Footers · min6.0 KB3.5 KB1.0 KBWith a footer
Space · min26.9 KB19.8 KB2.8 KBPadding, margin and gap utilities
Effects · min11.8 KB6.1 KB1.6 KBTransforms, filters, animations
Utilities · min16.8 KB8.9 KB2.3 KBOptional helpers
Fonts · min4.8 KB2.6 KB0.4 KBSelf-hosted @font-face
polyfish.js · min23.0 KB9.6 KB3.6 KBOptional enhancements
Bundle · min204.0 KB108.3 KB19.2 KBEvery 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.

headhtml
<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.

headhtml
<!-- 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.

this page has…
headyour selection
<!-- 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-Footers.min.css?v=50298de4">    4 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 loadedSeparate files, gzippedRequests
410.9 KB4
615.9 KB6
716.9 KB7
819.8 KB8
All ten, as the bundle19.3 KB1

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.

headhtml
<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:

app.csscss
@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.

htmlthat is the whole change
<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.

headhtml
<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:

projecttree
/
  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:

nginxconf
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.

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.