Skip to main content

Documentation

No results found.
Features

Accessibility Widget

A floating accessibility control on the public site — the round "person in a circle" button in the bottom-left corner. Visitors open it to adapt the page to what they need: bigger text, higher contrast, underlined links, a plain r...

A floating accessibility control on the public site — the round "person in a circle" button in the bottom-left corner. Visitors open it to adapt the page to what they need: bigger text, higher contrast, underlined links, a plain readable font, and paused animations. Their choices are remembered in their browser and re-applied on every page, before the page paints.

It is off by default — turn it on at Dashboard → Settings → General → Accessibility Widget, where you also choose the light (white) or dark bubble. It is core (not a paid add-on) and separate from the Accessibility Scanner: the scanner helps the site owner fix the site, the widget helps an individual visitor read it.

Why off by default. It is a visitor accommodation, not a compliance tool, and it cannot improve a Lighthouse or PageSpeed accessibility score — those measure the page in its default state, and this is opt-in. It is also a visible control on your client's site wearing the same icon, in the same corner, as the commercial accessibility overlays whose presence has been used as evidence against site owners; and modern browsers and operating systems already offer text scaling, contrast and reduced-motion settings globally. So it is offered rather than imposed: switch it on for the clients who want it.


What the visitor gets

Option What it does
Text size — Default / Large / Larger Scales the whole page's type by 112.5 % / 125 %. Spacing scales with it (the same way browser zoom behaves), so layouts don't break — text just gets bigger.
High contrast Measures every piece of text against what is actually painted behind it and, wherever the contrast falls short of WCAG AAA (7:1 for normal text, 4.5:1 for large text), nudges the color just far enough toward black or white to clear it. Text over photos, video, and gradients is left alone — the row's own overlay handles that — and brand backgrounds, images, and section colors keep their design. Focus outlines become a thick ring in the current text color.
Underline links Every body link gets an underline, so links are identifiable without relying on color. Buttons stay buttons.
Readable font Swaps every font — including decorative or script heading fonts — for the system's plain sans-serif, and neutralises wide letter-spacing.
Pause animations Stops CSS animations and transitions, entrance/stagger reveals, popup motion, animated canvas backgrounds, marquees, and autoplaying video — the same treatment the site already gives visitors whose operating system asks for reduced motion.
Reset Clears every choice (shown only when something is set).

The button carries an aria-label, the panel is a labelled dialog, every control reports its state with aria-pressed, opening moves keyboard focus into the panel, and Escape or clicking outside closes it.

Where it sits

Bottom-left, stacked above the language switcher and dark-mode toggle row when either of those is on, so each control keeps its own slot and the pop-ups open upward without overlapping. (Chat and the editor's edit bubble stay bottom-right.)


Checking it yourself (QA)

Two ways to verify the widget on a real page, neither of which requires changing your own settings or touching the dashboard.

Preview any setting from the URL

While signed in to the dashboard, add ?a11y= to any page on your site — it works whether or not the widget is switched on, so you can check a site without putting the bubble in front of visitors:

URL What you see
/about?a11y=contrast that page with High contrast applied
/about?a11y=all contrast + underlined links + readable font + paused animations
/about?a11y=text-xl the largest text size (text-lg for the middle step)
/about?a11y=contrast,links any combination, comma-separated
/about?a11y=off every option forced off, for an A/B comparison

It applies to that one page view only. It is never saved, so a link you send to a colleague won't leave their browser stuck in high contrast, and it changes nothing for your visitors. It reports the same canonical as the clean URL, so it can't affect SEO.

It requires being signed in, and that is load-bearing rather than a permission nicety: a11y is treated as a tracking-style parameter that shares the clean page's cache entry, which is only safe while both render identically. Signed-in requests never touch that shared cache, so previewing can neither bake the preview into the page other visitors get nor be silently served a cached page without it. Signed out, the parameter does nothing.

Get the numbers

For "is it actually fixing enough?", open the browser console on any page and run:

await window.wpA11yAudit()              // the widget's own AAA target
await window.wpA11yAudit({ level: 'AA' })   // the WCAG AA bar that Lighthouse checks

It measures every piece of text on the page before and after its adjustment and prints a summary plus a table of anything still short:

[a11y audit AAA] / — 201 measured, 169 already passing, 26 fixed by High contrast,
6 STILL FAILING, 5 unmeasurable (image/video/gradient backdrop). Worst ratio after: 5.69

Each failure is labelled so you can tell a limitation from a defect:

  • ceiling — the background is a mid-tone, so no text colour reaches the target over it; the engine went as far as pure black or white and stopped. bestPossible tells you the ratio that physically exists there. Fixing these means changing the background, which the widget deliberately never touches.
  • unmeasurable-backdrop — an image, video, or gradient sits behind the text, so there is no honest ratio to compute. The row's own overlay/scrim has to carry those.
  • under-target — a genuine miss worth reporting.

It restores your settings and scroll position when it finishes, so it's safe on a live page.

Why it scrolls. The fix is applied per element as it comes into view, because measuring what is truly painted behind text only works inside the viewport. Any tool that samples a single viewport — PageSpeed Insights (~412×823) and the dashboard's rendered-DOM scanner (a 1280×800 iframe) — therefore sees only the top of the page. On a 7,000px homepage that is about an eighth of the evidence, which is why the audit walks the whole document instead.

What this can and cannot tell you about PageSpeed Insights

It won't move your PSI score, and it isn't meant to. High contrast is a visitor opt-in, and PSI measures the page in its default state — so a contrast warning PSI reports is about the colours your visitors land on, not about the widget.

Use the Accessibility Scanner to find and fix those at the source: it runs axe-core's real ruleset per page and its source-level check names the exact field, the ratio, and both hex colours. Fixing it there improves the page for every visitor, which is the thing PSI is measuring.


Settings

Dashboard → Settings → General → Accessibility Widget

  • Show / hide switch — default off. Turning it on is the only thing that puts the bubble on your public site.
  • Button style — Light (white bubble, the default) or Dark. The style follows this choice, not the page's own light/dark mode, exactly like the language switcher and dark-mode toggle beside it.

Saving flushes the response cache so the change reaches cached pages immediately.


How it works (for developers)

Everything lives around App\Support\AccessibilityWidget, which owns the two setting keys (site.accessibility_widget_enabled, site.accessibility_widget_theme), the localStorage key (webprocms.a11y), the option → <html> class map, and the boot script.

Default. enabled() reads the setting with a false default and nothing ever writes that row, which is what lets the shipped default reach every install at once — while an install whose owner saved the settings card has a real row and keeps its own choice. Don't "fix" that with a migration that materialises rows.

Cache architecture. The public HTML is response-cached once per URL and served to every visitor, so nothing about the widget may vary per visitor server-side. All state is the visitor's localStorage. The head partial inlines AccessibilityWidget::bootScript() (through <x-inline-script>, so the CSP permits it) right after the dark-mode appearance script: it reads the stored choices, applies them as classes on <html> before first paint (no flash of the un-adjusted page), and defines window.wpA11y (get / set / reset) that the panel calls. Every change fires a wp-a11y-change window event so any listener — the panel, the contrast module — re-syncs. When the widget is turned off the script is not emitted at all, so a stored preference is ignored, mirroring the dark-mode toggle.

The classes — a11y-text-lg / a11y-text-xl, a11y-contrast, a11y-links, a11y-font, a11y-motion — are plain CSS in resources/css/public.css (the "Accessibility widget" block), which ships in the release bundle so node-free installs get it. Text size is a root font-size percentage (Typography's body/heading sizes are rem-based on body/h*, so they scale; media-query breakpoints do not). Pause-animations collapses motion to a single ~instant frame rather than animation: none, so entrance animations still resolve to their visible end state.

Reduced motion is one check. resources/js/shared/reduced-motion.js exports prefersReducedMotion() = the OS media query or the a11y-motion class; every JS runtime that used to test the media query directly (entrance/stagger in public.js, dl-popup.js, popups.js, collection-filter.js, animated-bg/) now calls it, and App\Support\EntranceAnimation inlines the same two-part test. Add any new motion runtime to that list — don't test the media query on its own.

High contrast is the one JS-driven option — resources/js/a11y-contrast.js, lazy-loaded by public.js only once the class is present. A token remap can't do this job: the same mid-tone shade is muted body copy on a white card and the caption on a dark footer, so darkening it fixes one and erases the other. The module instead measures each text-bearing element as it scrolls into view (elementsFromPoint, so an absolutely-positioned image sibling or overlay counts), composites the real backdrop, and applies the smallest inline color nudge that reaches AAA — tagged data-a11y-c with the prior inline value so it can be undone exactly. It re-measures on DOM mutations (Livewire morphs, collection swaps) and restores + re-runs on a dark-mode flip.

Critical CSS. The bubble renders after the footer but sits in the viewport at first paint, so it rides the homepage critical-CSS tail like the language pill; both settings are in HomeCriticalCss's signature so a change regenerates it.

Tests: tests/Feature/PublicAccessibilityWidgetTest.php, plus the fixed-overlay case in tests/Feature/HomeCriticalCssTest.php.