Craft: the accessibility engine

This portfolio is hand-written: no framework, no build step, one stylesheet and one script. It carries an accessibility layer of eleven preferences a visitor can set, restored before the first paint, plus a keyboard and focus model. This page covers how that works and what it changed about the design.

Role

Design and build, solo

Outcome

Eleven preferences, verified across three browser engines

Methods

WCAG 2.2 AA, automated auditing, keyboard and screen-reader testing

Context

Ongoing, self-initiated

Why this page exists

Accessibility is easy to claim on a portfolio and hard to check. This page documents the system the site actually runs, including the parts I got wrong first time.

Every number here is measured. Where a decision was wrong, it is written down as it happened.

Eleven preferences

Nine change how the page renders. Two change how it behaves: muting the interface sounds, and disabling the single-key shortcuts so speech input does not trigger them. They combine freely, and every pair was checked to confirm one preference never cancels another.

Reduce motion
Collapses transitions and animation, and switches smooth scrolling to instant. Also honoured automatically from the operating system setting.
High contrast
Raises the text tokens, including headings and emphasised text, not just body copy.
Larger text
Raises the root size so the whole type scale grows with it.
Reading guide
Wider letter and word spacing, taller line height, all driven by one inherited token.
Dyslexia-friendly type
Switches to a widely available everyday typeface with wider spacing. No specialist font to install.
Enhanced focus
A thicker, higher-contrast focus ring with its own dark-mode colour. Shown only on devices with a keyboard.
Mute sounds
Silences the interface audio. No visual change.
Underline links
Makes links visually distinct without drawing a rule through the filled buttons.
Reading line
Marks the line currently being read.
Hide images
Removes imagery to cut visual load, leaving the text structure intact.
Disable single-key shortcuts
Stops D and T acting as shortcuts, for speech input. No visual change.

Preferences have to survive the first paint

A preference restored by the main script arrives too late. The visitor sees the page render wrong, then correct itself, and the people who set the preference are the ones that flash affects most.

The restore runs in a small render-blocking script, first element in the head, byte-identical across every page. It reads the saved preferences and applies them before anything paints. Storage access is wrapped in a try/catch: a browser with storage blocked throws on read, and an unguarded read there takes the whole script down with it.

495 bytes

The render-blocking restore script. Identical on all pages, so it is a single hash in the Content-Security-Policy.

Decisions that changed because of it

Four of these were bugs in my first implementation.

Reading guide
Tightened the hero headlineLeaves it aloneThe mode restated an absolute letter-spacing that had drifted from the value the headline already declared, so a feature promising wider spacing delivered tighter. The fix was to delete the override.
Reading guide
Line height reached almost nothingInherits everywhereInheritance stops at the first element that declares its own value, so setting line-height on the root reached almost no text. Moving it to a token that the body reads lets it propagate normally.
Enhanced focus
Light ring in dark modeOwn dark-mode colourTheme and preference are both set on the root element, so a descendant selector could never match. Written as a compound selector, the dark ring applies: 7.25:1 against the dark background.
Underline links
Rule through the filled buttonButtons excludedA blanket underline treats a filled call-to-action as body text, so the buttons are excluded by name.
Comparison labels
Faded to 2.30:15.74:1The inactive label was dimmed with opacity, which halves effective contrast and is invisible to automated checkers. It now dims by colour, so the contrast can be measured.

Focus, dialogs and one lockout

The site has four overlays: the accessibility panel, a command palette, a shortcuts sheet and an image lightbox. While a modal one is open the rest of the page is marked inert, so keyboard and screen-reader users cannot wander behind it.

That model shipped with a bug. The accessibility panel is deliberately non-modal, but it was listed in the guard that decides when to release inert. Opening the panel, then opening and closing the palette, left the page permanently inert, and the only way back was to open the panel again. The guard now lists just the modal dialogs. A check that nothing is left inert after any overlay closes runs on every page, in every engine, on every test pass.

Keyboard

A command palette on Cmd or Ctrl K, a shortcuts sheet on question mark, D for theme, T for top. The single-key shortcuts are guarded so they do not fire while text is being typed or while a modifier is held, and they can be switched off entirely for speech input.

Two preferences only make sense with a keyboard: enhanced focus, and disabling single-key shortcuts. Both are hidden on touch devices.

How it is checked

Automated auditing across every preference, both themes, both palettes and both viewports, in three browser engines. Then the checks a scanner cannot do: text faded by opacity, composited and measured by hand; focus rings driven by real keyboard input; and the page loaded with JavaScript disabled to confirm no content is left invisible.

3browser engines: Chromium, WebKit and Firefox, desktop and mobile
11preferences, each verified to change a real rendered property and to combine without conflict
0outstanding WCAG 2.2 AA issues from automated auditing