Handled for you
| Concern | What the framework does |
|---|---|
| Focus visibility | A two-pixel accent ring on :focus-visible for every interactive element, declared with :where() so a component can override it without an !important fight. |
| Motion | prefers-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. |
| Contrast | Every role pairing is verified at AA or better in both themes, and re-verified on every build. |
| Touch targets | 44px minimum on coarse pointers, without inflating the desktop layout. |
| Zoom | No page blocks zooming, and fields are floored at 16px on touch so iOS does not zoom on focus and strand you there. |
| High contrast mode | forced-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.
Appears for keyboard users, stays out of the way of mouse clicks.
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.
| Pairing | Dark | Light | Requirement |
|---|---|---|---|
| Body text on page | 10.35 | 14.77 | 4.5 (AA) |
| Muted text on page | 5.49 | 6.72 | 4.5 (AA) |
| Accent link on page | 6.70 | 6.19 | 4.5 (AA) |
| Label on accent button | 9.31 | 7.16 | 4.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.
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
h1toh6; it cannot know whether yourh3should have been anh2. Oneh1per page, no skipped levels. - Accessible names. Every control needs a
<label for>, anaria-labelor anaria-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 anaria-labelon 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.
| Attribute | Styles |
|---|---|
aria-current | Navigation underline, pagination fill, sidebar and list-group highlight |
aria-invalid | Field border and focus ring colour |
aria-pressed | Selected button in a group |
aria-disabled | Dimmed pagination item, without removing it from the tab order the way disabled does |
aria-busy | Button spinner and pointer lock |
aria-sort | Sort 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.