Skip to content
PolyFish CSS

JavaScript

One optional file, 9.6 KB minified and 3.6 KB gzipped. Every component works without it. Nothing here creates behaviour that did not exist — it finishes behaviour that CSS could only take so far.

The contract

Adding a script to a framework that spent five versions not having one deserves an explicit set of rules. These are enforced by how the code is written, not by intention:

  • No element is hidden by JavaScript on load. If a script hides content and then fails to load, the content is gone. Nothing here hides anything CSS was not already hiding.
  • No enhancement is the only route to a feature. The menu opens without it. The tabs switch without it. The dialog opens without it, via :target.
  • Every enhancement is opt-in per element, through a data attribute. Adding the file to a page changes nothing until you mark something up.
  • Nothing injects CSS. The two elements the script creates — a copy button and a sort button — are styled in the Content module. A script that writes styles is a script that fights your Content-Security-Policy.

What changed in the policy

This site's CSP moved from script-src 'none' to script-src 'self'. That is the honest cost. In exchange the script tag carries a sha384 subresource-integrity hash, so a stale or truncated deploy fails loudly instead of half-running.

Adding it

headhtml
<script src="/polyfish.min.js?v=a39398cb" defer></script>

defer, not async: the DOM has to exist, and there is nothing here worth blocking the parser for. Then mark up the elements you want upgraded — everything else is untouched.

Enhancements

AttributeOnWhat it addsWithout it
data-pf-menudetails.pf-menuCloses on outside click and Escape, keeps aria-expanded in syncOpens and closes on its summary
data-pf-tabs.pf-tabsFull ARIA tablist: roles, aria-selected, roving tabindex, Home and EndRadios switch panels, arrows already work
data-pf-dialogAny triggerNative showModal(), focus trapping, focus returned on close:target dialog, no focus trap
data-pf-copy.pf-codeA copy button and a polite status messageSelect and copy by hand
data-pf-countertextareaLive character count against maxlengthThe browser still enforces the limit
data-pf-dismissAny buttonRemoves its closest alertThe alert stays
data-pf-nosortthOpts one column out of sorting; data-pf-value on a cell sorts on that instead of its text
data-pf-sorttableClick-to-sort headers with aria-sort, numeric-awareThe table renders in source order
data-pf-spy.pf-sidenavMarks the current section with aria-currentThe sidebar is a plain list of links
data-pf-dialog-closeAny button in a dialogCloses the native dialog it sits insideUse form method="dialog" or the CSS-only close link
data-pf-drawer.pf-drawerEscape closes it, focus moves into the panel and back to the trigger, and the page behind it stops scrollingOpens and closes on :target
data-pf-theme-toggleAny buttonCycles auto, light, dark and remembers the choice. Put data-pf-theme-label on a child to have its text updated tooThe palette follows the operating system

That table is sortable — click a header. So is the module table on the installation page. The theme button in the navigation, the copy buttons on every code block and the highlighted item in the sidebar are the same script.

The one exception

PolyFish.toast() is the only thing here with no CSS-only version, because a toast with no script is an empty box. It is documented as requiring JavaScript rather than quietly degrading to nothing.

toastjs
PolyFish.toast("Check created");
PolyFish.toast("Could not save", { variant: "danger", duration: 0 });

The region is role="status" with aria-live="polite", which queues the message behind whatever a screen reader is currently saying. An assertive region interrupts mid-sentence — for a confirmation that is rude rather than urgent. Hovering or focusing a toast cancels its removal, because a message that vanishes while being read is worse than one that stays. duration: 0 keeps it until dismissed.

Build your attribute list

Tick the enhancements you want. Everything you leave off costs nothing — the script walks for the attributes and finds none.

enhancements
markupyour selection
<!-- the script itself, 7.9 KB -->
<script src="/polyfish.min.js?v=a39398cb" defer></script>

<!-- nothing ticked: the script loads and does nothing at all --><details class="pf-menu" data-pf-menu>            closes on outside click and Escape<div class="pf-tabs" data-pf-tabs>                 roles, aria-selected, roving tabindex<button data-pf-dialog="confirm-id">          showModal, focus returned on close<div class="pf-code" data-pf-copy>                adds a copy button and a status region<textarea maxlength="280" data-pf-counter="count-id"> live count<button data-pf-dismiss=".pf-alert">          removes its closest alert<table data-pf-sort>                          click-to-sort with aria-sort<aside class="pf-sidenav" data-pf-spy>           marks the current section<button data-pf-theme-toggle>               auto / light / dark, remembered

Checkboxes and :has(). Every line above is opt-in per element — the file changes nothing until you add one.

The API

Three properties, and only the first one matters.

consolejs
// upgrade a container after injecting markup
PolyFish.init(document.querySelector('#results'));

// idempotent: elements record what they already have,
// so nothing is bound twice
PolyFish.init();

PolyFish.version;         // "0.5.0"
PolyFish.reducedMotion(); // true if the user asked for less motion

Tests

The script ships with a jsdom suite of 69 assertions. Twelve of them are not about features at all — they check that the page survives the script's absence: that the tabs still have a checked radio, that no element carries a hidden attribute, and that nothing depends on a class the script adds.

Two more cover failure modes rather than success: blocked localStorage, which throws outright in private mode in some browsers, and a missing clipboard API, where the copy button is not created at all rather than rendered dead.

What is still not here

No carousel, no toast queue, no combobox, no scroll animation library. The line has not moved: this file finishes things the CSS started. A component that only exists when the script runs would break the first rule on this page.


Next: the component reference.

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.