Skip to content
PolyFish CSS

Accessibility

What the framework does for you, what it deliberately refuses to do, and the things no stylesheet can do on your behalf. A CSS framework cannot make a page accessible — it can remove a set of standard ways to make one inaccessible.

Handled for you

ConcernWhat the framework does
Focus visibilityA two-pixel accent ring on :focus-visible for every interactive element, declared with :where() so a component can override it without an !important fight.
Motionprefers-reduced-motion drops every transition and animation in every module to 0.01ms and disables smooth scrolling. A new component cannot forget to honour it.
ContrastEvery role pairing is verified at AA or better in both themes, and re-verified on every build.
Touch targets44px minimum on coarse pointers, without inflating the desktop layout.
ZoomNo page blocks zooming, and fields are floored at 16px on touch so iOS does not zoom on focus and strand you there.
High contrast modeforced-colors reinstates borders and honours system keywords, so buttons and cards do not flatten into the page.
Skip link.pf-skip is off-screen until focused, then lands on top of the navigation.
Hidden content.pf-visually-hidden uses clip-path rather than display: none, so the text stays in the accessibility tree.

Focus you can actually see

Tab through the two panes below. The right one has the ring removed — the single most common accessibility regression in a redesign, and usually introduced on purpose because someone thought the outline was ugly.

:focus-visible ring

Appears for keyboard users, stays out of the way of mouse clicks.

outline: none

Keyboard focus is now invisible. There is no way to tell where you are on the page.

Contrast, measured rather than eyeballed

Both palettes are checked on every build. These are the numbers as shipped.

PairingDarkLightRequirement
Body text on page10.3514.774.5 (AA)
Muted text on page5.496.724.5 (AA)
Accent link on page6.706.194.5 (AA)
Label on accent button9.317.164.5 (AA)

This is also why the accent darkens in light mode rather than staying put: the pastel green reads 6.7:1 on deep blue and 1.6:1 on near-white. A palette that does not change with its background is decoration, not a palette.

See the guards working

Three of the accommodations in this module are invisible until the condition applies. These switches apply them on demand, so you can check the behaviour without changing your operating system settings.

overlays

Hairlines and muted text

Turn on the contrast switch: this border goes solid and this copy moves up to near-full strength. Those are the exact values the prefers-contrast: more block declares.

The ring comes from :focus-visible, so it appears for keyboard focus and stays out of the way of a mouse click.

Checkboxes and :has(). The overlay draws a 44px box centred on each control — the minimum a finger needs.

Still yours

No stylesheet can do these, and a framework that implies otherwise is selling something.

  • Heading order. The framework styles h1 to h6; it cannot know whether your h3 should have been an h2. One h1 per page, no skipped levels.
  • Accessible names. Every control needs a <label for>, an aria-label or an aria-labelledby. An icon button without one announces as "button" and nothing else.
  • Alt text. Empty alt="" for decoration, a description for anything that carries meaning. There is no third option, and a missing attribute is not the same as an empty one.
  • Landmarks. <header>, <nav>, <main>, <footer>, and an aria-label on a second nav so the two can be told apart.
  • Language. <html lang> decides which voice a screen reader uses. Getting it wrong makes a page unintelligible rather than merely wrong.
  • Link text. "Read more" four times on a page is four identical entries in a screen reader's link list.

Attributes that carry the styling

Several components style themselves from ARIA state rather than a modifier class. This is deliberate: a class and an attribute can disagree, and when they do it is the attribute that assistive technology believes.

AttributeStyles
aria-currentNavigation underline, pagination fill, sidebar and list-group highlight
aria-invalidField border and focus ring colour
aria-pressedSelected button in a group
aria-disabledDimmed pagination item, without removing it from the tab order the way disabled does
aria-busyButton spinner and pointer lock
aria-sortSort direction marker on a table header

Known limits

Three places where the CSS-only version is honestly weaker, all documented in the source of the component rather than here alone.

Tabs

Without the script they announce as a radio group, not a tablist. Arrow keys work either way. data-pf-tabs adds the full pattern.

Dialogs

The :target fallback does not trap focus. Use the native <dialog> whenever a script is available — it traps focus and handles Escape itself.

Menus

A <details> menu does not close on an outside click without a listener, and it is not an ARIA menu. For a true menu widget you need the script and the roles.

Automated checks are a floor, not a ceiling

Everything on this page is verified by a script on every build, which catches contrast, missing labels, duplicate ids, heading counts and dead anchors. It cannot tell you whether your alt text is useful, whether your tab order makes sense, or whether the page is usable at 200% zoom. Test with a keyboard and a screen reader before you ship.


Next: what the framework costs and what to load.