← Back to the résumé

Accessibility audit

Zero violations, twenty-six times over.

This résumé is one page with a lot of states: a sidebar drawer, a search palette, five dialogs, two themes, two viewports. Every combination is run through axe-core, the same engine commercial auditors use, and then through a scripted manual pass for the things automation can't see. The audit runs on every build; the numbers on this page are the current result.

Standard
WCAG 2.2 A + AA + AAA
States tested
26 / 26
Violations
0
Audited with
axe-core

How we tested

A conformance claim should say exactly what was tested and how. This one does.

A headless Chromium loads the résumé, waits for React to hydrate, and runs axe.run() with the WCAG 2.2 A/AA tag set, then again with the AAA set. It does that for the base page, the open navigation drawer, the open search palette, and each of the five dialogs (Privacy, License, Contact, Search feature, One-page résumé) — in light mode and dark mode, at 1280 px and 390 px. Seven states × two themes × two viewports = twenty-six runs. Current result: zero violations at every level.

Axe catches roughly half of real-world defects. The other half we script: a full keyboard traversal with a focus-indicator check on every tab stop, dialog focus management (focus in, Tab trap, Escape, background inert, focus return), the search combobox's aria-activedescendant, reflow at 320 px, the WCAG text-spacing override, 200 % text size, prefers-reduced-motion, a no-JavaScript render, and both print paths. And a colour-parity lint that fails the build if any colour outside @media print is hard-coded instead of drawn from a theme token.

Every state passes

Each tile is one axe-core run. Hover or tab to it for the theme, viewport and UI state it represents.

    What we test, and what we do about it

    The success criteria that actually bite on a page like this, grouped by WCAG's four principles.

    Perceivable

    • 1.1.1A Every image has a purpose-first alt; decorative glyphs are alt=""; QR codes say what scanning does.
    • 1.3.1A One <h1>; heading levels computed from nesting depth, never hard-coded. Tables carry a caption and scope. Lists contain only <li>.
    • 1.4.1A The current section in the sidebar is aria-current, not colour alone.
    • 1.4.3AA Every text/background pair computed, not estimated. See the palette below.
    • 1.4.4AA At 200 % text size the header wraps instead of pushing Contact and Download off-screen.
    • 1.4.6AAA Every pair in both themes clears 7:1.
    • 1.4.10AA No horizontal scroll at 320 px; only wide tables scroll, inside labelled, focusable regions.
    • 1.4.11AA The search box's boundary uses a 3:1 border token.
    • 1.4.12AA Nothing clips under the text-spacing override.
    • 1.4.13AA The headshot and Signal QR cards open on keyboard focus as well as hover, and Escape dismisses them.

    Operable

    • 2.1.1A Every control is a real <button> or <a>. Code and table scrollers are focusable.
    • 2.1.2A Dialogs trap Tab and release on Escape or Close.
    • 2.1.4A No unmodified single-key shortcuts. Search is Ctrl/+K; the palette's letter shortcuts only work while its own input has focus.
    • 2.2.2A Nothing animates forever. The landing flash lasts two seconds.
    • 2.3.3AAA All animation, transitions and smooth scrolling are gated on prefers-reduced-motion, in CSS and in JS.
    • 2.4.1A Skip link moves focus to <main>.
    • 2.4.3A The closed drawer is inert — no tabbing through eleven off-screen links. Dialogs receive focus on open and return it on close.
    • 2.4.7AA No outline: none on any control.
    • 2.4.11AA Nothing focusable is ever off-screen.
    • 2.5.8AA Stacked sidebar links are ≥ 24 px tall; isolated small targets have clear space.

    Understandable

    • 3.1.1A lang="en".
    • 3.2.2A Nothing changes context on input; search navigates only on Enter or click.
    • 3.2.4AA Contact, Privacy and License are reachable the same way from the header and footer.
    • 3.2.5AAA Every link that opens a new tab says so in its accessible name — added automatically, even for links written in raw HTML content.
    • 3.2.6A Help (Contact) sits in the same place on every state of the page.

    Robust

    • 4.1.2A The search palette is a true ARIA 1.2 combobox: role on the input, aria-activedescendant tracking the arrow keys, options with ids, wrapped in a labelled dialog. The theme toggle is aria-pressed. The menu button has a name and aria-expanded.
    • 4.1.3AA A polite live region announces result counts, "Copied", and theme changes.
    • Server-rendered HTML matches the client render — no hydration mismatch, so assistive tech sees one stable tree.

    The contrast palette

    Every text-on-background pair the résumé uses, in both themes, computed against WCAG's relative-luminance formula. All twenty clear AAA.

    Light

      Dark

        Beyond the standard

        Things WCAG doesn't require, which we do anyway.

        AAA, not just AA

        The light theme's brand green moved from emerald-500 (2.5:1) to emerald-800 (7.7:1). Every text pair in both themes clears 7:1.

        Both themes, every state

        Twenty-six axe runs per build. A page that passes in light and fails in dark hasn't passed.

        Colour-parity lint

        The build fails on any hard-coded palette class or hex/rgba literal outside print. A dark-only colour can't get back in.

        Search lands on the item

        A hit on a table row, bullet, tag or card scrolls to that element, expands its "Show more" list if it was collapsed, focuses it, and flashes it — not the section top.

        One dialog contract

        All five dialogs, the search palette and the mobile drawer share a single useDialog hook: focus in, trap, Escape, background inert, focus return.

        No flash of wrong theme

        An inline script applies your saved or OS colour scheme before first paint. Works with JavaScript off. Server and client render identically.

        Reduced motion, everywhere

        One CSS gate for every animation and transition, and the same media query checked by every scrollIntoView and framer-motion variant.

        Authors can't forget

        New-tab hints, table captions for untitled tables, and per-item anchors are generated by the engine from content, not typed by hand.

        Self-hosted, zero trackers

        Fonts and scripts are bundled with the site. Nothing is fetched from a CDN, nothing phones home — including the search, which runs entirely in your browser.

        The journey to zero

        This page didn't start at zero. The first scan of the original build, with the real content, looked like this.

        First scan, A/AA hits across 26 states
        397
        Distinct axe rules failing
        9
        AAA contrast failures on top
        42
        Defects only the manual pass could see
        24
        After fixes — A, AA and AAA
        0

        axe-core reports at most six elements per rule per run, so 397 is a floor. The 24 manual findings included unmanaged dialog focus, an off-screen but tabbable drawer, a search combobox with no aria-activedescendant, a global / shortcut, and hover-only content.

        What we don't claim

        • No testing with real assistive-technology users. The screen-reader model here is the accessibility tree and ARIA semantics, verified by script, not by a person using NVDA, JAWS or VoiceOver day to day.
        • No cognitive-load testing. Reading level and information density were not assessed.
        • Target size (Enhanced) isn't met. WCAG 2.5.5 wants 44 × 44 px; several controls are 24–32 px. The AA criterion (2.5.8) is met.
        • Print output is checked for page count, not conformance. WCAG doesn't apply to paper, but the CV and one-page résumé print paths are verified to produce the expected pages.
        • Content is linted structurally, not editorially. Raw-HTML blocks are checked for images without alt, headings, malformed anchors and vague link text; whether the prose itself is clear is a human judgement.
        • Automated tools miss things. Zero axe violations plus a scripted manual pass is a strong claim, not a complete one. A failing test we missed is more valuable to us than ten compliments — tell us.

        Standards alignment

        This work aligns with the international and regional accessibility regulations that reference WCAG 2.2 as their technical baseline.

        WCAG 2.2

        W3C Recommendation
        October 2023

        ISO/IEC 40500:2025

        ISO/IEC ratification
        of WCAG 2.2

        EN 301 549

        EU accessibility
        for ICT (EAA)

        Section 508

        US Federal
        Rehabilitation Act

        UK PSBAR

        UK public-sector
        accessibility regs

        Audit framework: axe-core. Test runner: Playwright + headless Chromium. Last audited: .