Skip to main content

PayslipIQ Accessibility Statement

Statement prepared on 2026-05-05; last updated 2026-09-26. Last manual accessibility pass: 2026-08-04. Next scheduled review: 2027-02-04.

Conformance status

PayslipIQ partially conforms with the Web Content Accessibility Guidelines (WCAG) 2.2 at Level AA at the date of this statement (2026-05-05). ‘Partially conforms’ means that some parts of the content do not yet fully meet the conformance level.

Of the 55 Level A and Level AA Success Criteria in WCAG 2.2, we self-assess 39 as conformant, 4 as partially conformant, 12 as not applicable to the current product surface. Per-criterion evidence is published below.

Standards we follow

  • WCAG 2.2 - W3C Recommendation, October 2023. Primary technical standard for this statement.
  • EN 301 549 v3.2.1 - the harmonised European standard for ICT accessibility, which incorporates WCAG 2.1 AA by reference and is the basis of conformance for our Irish audience. See our Ireland accessibility statement for the EU-specific framing.
  • Section 508 - informational. We track US Federal Section 508 alignment because it shares technical requirements with WCAG 2.0 AA, but our primary regulatory frame is the UK Equality Act 2010.

Measures we take

  • Editorial standards mandate accessible-by-default templates for every new page, calculator and tool.
  • The design-system palette is built to a minimum 4.5:1 contrast ratio for body text and 3:1 for large text, input borders and focus rings. Every pairing is measured rather than estimated, and the measured ratio is recorded next to the token it belongs to in src/app/globals.css. There is no build step that re-measures them; what runs automatically is axe’s colour-contrast rule, which the CI suite is forbidden from disabling. From 26 September 2026 it runs over twenty-three pages on every pull request rather than six: the six anchor pages plus one page per template, including the six that carried a defect in that day’s sweep.
  • Every form input is paired with a programmatically associated<label htmlFor>; error messages are linked through aria-describedby and announced via role=“alert”. Label association is verified automatically on the six anchor pages by the axe run described below; on the remaining templates it rests on the manual pass and on review, because no lint rule in our configuration checks it.
  • Two regions are pinned to the viewport: the header (sticky top-0 in src/components/layout/Header.tsx, on every page) and, below the md breakpoint only, the bottom call-to-action bar on the checker and calculator pages. The bar takes its link out of the tab order in the same state change that hides it, so it never holds focus while invisible. Anything the browser scrolls to — an in-page link’s target, or the next control when you press Tab or Shift+Tab — clears both of them, because src/app/globals.css declares html { scroll-padding-top: 5rem } for the header and a consent-bar-aware scroll-padding-bottom that reads the bar’s own measured height (SC 2.4.11). Until 26 September 2026 this was applied per element, with pq-anchor-offset or pq-anchor-offset-sm on 28 files, which covered anchor links and nothing else: Shift+Tabbing backwards up a page landed between one and seven controls per page entirely underneath the header. Those per-element classes remain; the global rule is what the criterion now rests on.
  • Focus is visible on interactive elements by two mechanisms, not one: the design-system controls draw a 3px :focus-visible outline in src/app/globals.css, and components elsewhere use Tailwind’s focus-visible:ring-2 utility. There is no single selector covering the whole site.
  • Keyboard and screen-reader focus is moved on client-side route changes. Next.js does not do this by itself: it swaps the page without a document load, so focus would otherwise stay on the link that was just activated. src/components/RouteFocusReset.tsx is mounted in the root layout and moves focus to <main id="main-content"> on every pathname change (SC 2.4.3). It deliberately does not fire on the first paint, because a full page load already puts focus at the top of the document and re-focusing would announce the page twice. The skip-to-content link targets the same element, so both entry points land in the same place. Verified in Chromium on 2026-09-25 by following an in-app link, confirming the navigation was client-side, and reading back document.activeElement; no automated test covers it, so a future refactor that unmounts the component would not be caught by CI.
  • We use semantic HTML5 first — landmarks, headings, lists, buttons, links — and add ARIA only where semantic HTML is insufficient: the role="status" live regions on the three checker forms, error associations via aria-describedby and aria-invalid, and aria-current on the final breadcrumb. This bullet used to say current-page indication generally; no navigation link on the site carries aria-current, and it is named here only where it is really used.
  • Motion respects prefers-reduced-motion and is suppressed for users who request reduced motion at the OS level.

Conformance by WCAG 2.2 principle

The four sections below cover every Level A and Level AA Success Criterion in WCAG 2.2, including the new 2.2 additions: SC 2.4.11 Focus Not Obscured (Minimum), SC 2.5.7 Dragging Movements, SC 2.5.8 Target Size (Minimum), SC 3.2.6 Consistent Help, SC 3.3.7 Redundant Entry, and SC 3.3.8 Accessible Authentication (Minimum). SC 2.4.13 Focus Appearance and SC 3.3.9 Accessible Authentication (Enhanced) are Level AAA and discussed under ‘AAA aspirations’ below.

Perceivable

Users can perceive the information being presented - it is not invisible to all of their senses.

SC 1.1.1 (Level A)Partial

Non-text Content

Decorative imagery is rendered via CSS gradients or marked aria-hidden. Meaningful imagery goes through src/components/OptimisedImage.tsx, whose props are Omit<ImageProps, 'src' | 'sizes'>, so next/image's required `alt` stays required and an image with no alt text is a type error rather than a review miss. The /api/og social card is consumed only by crawlers and carries an alt string in the openGraph metadata in src/app/layout.tsx.

  • The Open Graph card alt text is one fixed string set on the root layout, not authored per route.

Remediation target: 2026-12.

SC 1.2.1 (Level A)Not applicable

Audio-only and Video-only (Prerecorded)

PayslipIQ hosts no prerecorded audio-only or video-only media. All explanatory content is HTML text or static imagery.

SC 1.2.2 (Level A)Not applicable

Captions (Prerecorded)

No prerecorded synchronised media is published on the site.

SC 1.2.3 (Level A)Not applicable

Audio Description or Media Alternative (Prerecorded)

No prerecorded video content exists.

SC 1.2.4 (Level AA)Not applicable

Captions (Live)

PayslipIQ does not broadcast live audio or video.

SC 1.2.5 (Level AA)Not applicable

Audio Description (Prerecorded)

No prerecorded video content exists.

SC 1.3.1 (Level A)Conformant

Info and Relationships

Pages use semantic HTML5 landmarks (header, nav, main, footer, article, section). Forms in src/components/PayslipForm.tsx and src/components/IePayslipForm.tsx pair every input with a programmatic <label htmlFor>; error messages are linked through aria-describedby and surface aria-invalid when validation fails. One structural defect was found by the 2026-09-26 77-route sweep and fixed: the results list on /ie/calculators/pension put each <dt> inside a second wrapper <div>, and a <div> inside a <dl> may hold only <dt> and <dd>, so the terms sat two levels below their own list - axe definition-list (1 node) and dlitem (6 nodes), both wcag131, serious. The wrapper is gone and the hint text now lives inside the <dt>. That page is in TEMPLATE_PAGES in tests/a11y.spec.ts from the same date.

SC 1.3.2 (Level A)Conformant

Meaningful Sequence

Reading order matches DOM order across the site; CSS layout is built with flow-friendly Tailwind utilities and no absolute-positioned content out of source order. Verified manually against Firefox Reader View on the calculators and tools surfaces.

SC 1.3.3 (Level A)Conformant

Sensory Characteristics

Form errors and CTAs are conveyed through text plus colour plus icon, never through colour or position alone. Reviewed in src/components/PayslipForm.tsx (error rows render <p role="alert">) and src/components/AnomalyAlert.tsx.

SC 1.3.4 (Level AA)Conformant

Orientation

No CSS or JavaScript locks the viewport to a single orientation: there is no orientation media query or screen-orientation lock anywhere in the codebase, and src/app/layout.tsx sets no maximum-scale on the viewport. Layout is Tailwind's responsive grid, which reflows in both portrait and landscape down to 320px viewport width.

SC 1.3.5 (Level AA)Partial

Identify Input Purpose

Inputs that map to a WCAG 2.1 autocomplete token carry it: autoComplete="email" on the contact form (src/components/forms/ContactForm.tsx) and the email capture, and name / organization / organization-title on src/components/forms/PartnerEnquiryForm.tsx. Bespoke payroll fields (gross_period, tax_code) have no standard token and rely on labelling alone, which the criterion permits.

  • Not every input that could carry a standard token has been audited for one; the coverage that exists was added per form rather than from a sweep.

Remediation target: 2026-12.

SC 1.4.1 (Level A)Conformant

Use of Colour

Status, error and warning states always include a text label or icon; colour is only used as a redundant cue. The DeductionRow and AnomalyAlert components combine colour with leading symbols and copy. This held on the six anchor pages and did not hold everywhere: the 2026-09-26 77-route sweep reported axe link-in-text-block, serious, on all eight /partners/* subpages. The "portal home" link in src/components/partners/SubpageShell.tsx was text-emerald-300 inside a text-slate-300 sentence with underline only on hover, and emerald-300 against slate-300 is 1.02:1 - colour was the only thing distinguishing it, at a ratio the 3:1 exemption does not cover. Its underline is now permanent and the sweep is clean on all 77 routes; /partners/rate-card is in TEMPLATE_PAGES in tests/a11y.spec.ts from the same date so the eight cannot regress together again.

SC 1.4.2 (Level A)Not applicable

Audio Control

No audio plays automatically on any page.

SC 1.4.3 (Level AA)Conformant

Contrast (Minimum)

The design-system tokens meet the criterion and their measured ratios are recorded beside each one in src/components/ui/tokens.ts and in the :root block of src/app/globals.css - ink #0A2540 at 15.54:1, body #334155 at 10.35:1, muted #475569 at 7.58:1, and the primary button hover token --pq-action-hover #065f46 at 7.68:1 with white text. This entry was downgraded to partial earlier on 2026-09-26, when axe was pointed at fourteen pages outside the six anchors and found 36 real violations on two of them. Every one is fixed and it is conformant again, same date. What was wrong, and what each surface measures now in Chromium 1194 with axe-core 4.11.4 against a production build: (1) /tax-codes/compare carried all 35 - 14 nodes of Tailwind text-gray-400 #9CA3AF on white at 2.53:1 and 20 more of the same class on the #F9FAFB combo cards at 2.42:1, now text-gray-600 #4B5563 at 7.56:1 and 7.23:1; plus one text-gray-500 #6B7280 on the .section-dark band #0B0B0F at 4.06:1, which being light-on-dark needed a LIGHTER grey and is now text-gray-400 #9CA3AF at 7.74:1. text-gray-500 was rejected for the light surfaces: 4.83:1 on white but 4.63:1 on #F9FAFB, too near the floor. (2) The enquiry submit button in src/components/forms/PartnerEnquiryForm.tsx was white on emerald-500 #10B981 at 2.53:1, with hover emerald-400 #34D399 lower still; it is now the emerald-600 project token #047F5C at 5.01:1, hover emerald-700 #047857 at 5.48:1. That form mounts on /partners AND /partners/contact, which carried the same single violation. (3) A 77-route sweep the same day found a fourth instance of the money-green family the fourteen-page set never reached: three nodes of text-emerald-700/80 on /ie/calculators/pension, where the opacity modifier composites #047857 over white to #369379 at 3.74:1. The modifier is removed; 5.48:1. axe now reports zero colour-contrast violations on all 77 routes. Separately, 40 nodes on /tax-codes/compare and /partners that axe files as undecidable (obscured, overlapped, or SVG <text> it does not evaluate at all) were measured by hand from rendered pixels: 38 passed between 4.76:1 and 18.72:1, and the 2 that failed - fill='#10B981' on "image not stored" at 2.41:1 and on the refund figure at 2.54:1 in src/components/partners/HeroDashboard.tsx - are fixed to #047857 at 5.21:1 and 5.48:1. All six pages that carried a defect are now in TEMPLATE_PAGES in tests/a11y.spec.ts, so they are checked on every pull request rather than on the next manual sweep.

  • Automated coverage is 23 pages on every pull request (6 anchors + 17 templates in tests/a11y.spec.ts) and 77 routes in the 2026-09-26 sweep, out of roughly 2,030. Four separate instances of one defect - a green too light for white or for a light tint - were each believed to be "one page" until someone widened the net: green-600 (13 Aug, 354 nodes), emerald-600 (26 Sep, 31 nodes), amber-600 (26 Sep, the Irish deduction figures) and the three above. Outside the swept routes this claim rests on the design-system tokens being used, not on a measurement.
  • axe does not evaluate SVG <text>, ::placeholder or ::marker, so three classes of real text carry no automated check at all. All three were measured by hand on 2026-09-26 and what failed was fixed. SVG <text>: 45 elements in 12 files, 11 distinct fills, two below the floor, both in src/components/partners/HeroDashboard.tsx, both now #047857. Placeholders: the /tax-codes search field at 2.43:1 (gray-400 on #F9FAFB, now gray-600 at 7.23:1), and the cmd-K modal in src/components/SiteSearch.tsx plus src/components/EmailCapture.tsx at 2.56:1 (slate-400 on white, now slate-600 at 7.58:1) - the cmd-K one renders on every route. Markers: the ordered list on /about at 2.54:1 (gray-400, now gray-600 at 7.56:1); the other decimal markers on the site are slate-500 at 4.76:1 and pass.
  • Deliberately NOT treated as a failure: disc bullets styled marker:text-slate-400, which measure 2.56:1 on white on /trust/correction-policy and /trust/editorial-policy, and an inherited #D1D5DB bullet at 1.47:1 on /about. A disc carries no information - the list structure and the item text do - so it is pure decoration under the SC 1.4.3 exception, unlike a decimal marker, which is generated text a reader has to read. Recorded here because the measurement exists and someone will otherwise find it and think it was missed.
SC 1.4.4 (Level AA)Conformant

Resize Text

The whole type scale is expressed in rem, so it tracks the user's browser font size: see the `typeScale` export in src/components/ui/tokens.ts and the Tailwind classes it emits. No font size is set in px. Content reflows without loss of function up to 200% browser zoom.

SC 1.4.5 (Level AA)Conformant

Images of Text

No image of text is published anywhere on the site: there is no <img>, next/image or background-image carrying text in any template. The wordmark, which SC 1.4.5 would exempt in any case, does not need the exemption - src/components/layout/Header.tsx renders it as live text ("Payslip" plus a <span> for "IQ"), so it resizes, reflows and recolours with everything else. (This entry previously said the logotype was "delivered as SVG with an accessible name". There is no logotype SVG; public/icon.svg is the favicon and public/hero-payslip.svg is a decorative illustration.)

SC 1.4.10 (Level AA)Partial

Reflow

Measured 2026-09-26 in Chromium at a 320px viewport across 14 pages on both surfaces: 12 produced no horizontal page scroll at all (document.scrollWidth equal to clientWidth), and the two that did were this statement and its Irish counterpart, where inline <code> elements holding long file paths could not wrap - 10px of overflow here, 60px there. Both were fixed in the same change - break-all on the inline <code> elements, plus `overflow-wrap: anywhere` scoped to the statement <article> through the .pq-wcag-statement rule in src/app/globals.css, because the per-criterion evidence prose quotes file paths, CSS declarations and header values that no browser will break and only `anywhere` affects the min-content sizing the grid and definition-list tracks were refusing to go below. All 14 pages now measure zero overflow at 320px. The wide comparison table on /tax-codes/compare scrolls inside its own overflow-x container, which SC 1.4.10 permits for data tables; the page around it does not scroll.

  • Verified at 320px on 14 pages, not on all ~2,000 routes. The programmatic city and role templates were not individually measured.

Remediation target: 2026-12.

SC 1.4.11 (Level AA)Conformant

Non-text Contrast

Form input borders and focus indicators meet the 3:1 ratio against adjacent colours. The focus colour is --focus: #0a5aff in src/app/globals.css, measured at 5.36:1 on white in src/components/ui/tokens.ts, drawn as a 3px outline with a 2px offset. The input border token is slate-500 #64748B at 4.76:1 (src/app/globals.css, .pq-input).

SC 1.4.12 (Level AA)Conformant

Text Spacing

No inline style or utility class hard-codes line-height, letter-spacing or word-spacing in a way that prevents user override; all typography is set via Tailwind classes that respect cascade overrides.

SC 1.4.13 (Level AA)Not applicable

Content on Hover or Focus

No published surface shows author-styled additional content on hover or focus. This entry previously cited tooltips on src/components/TaxCodeBadge.tsx; that component renders a static span with a visually hidden status name and has no popover. The only hover text on the site is the native title attribute on src/components/TrustStrip.tsx, and SC 1.4.13 expressly exempts content whose appearance is controlled by the user agent. A hover-only dropdown does exist in src/components/IENavigation.tsx, but that file is imported by no page and so reaches no reader; if it is ever mounted this entry must be re-opened.

Operable

Users can operate the interface - the controls and navigation do not require interaction the user cannot perform.

SC 2.1.1 (Level A)Conformant

Keyboard

Every interactive control is a native button, link, input, select or textarea. Custom controls (e.g. the cookie banner) are reachable and operable via Tab, Shift+Tab, Enter and Space. A box that scrolls is also a keyboard target under this criterion, and until 2026-09-28 many were not: measured with JavaScript disabled against the production build, identical in Chromium 1194 and WebKit 2272, 94 scrolling regions on 48 pages could be panned with a mouse and not with a keyboard - 19 on 9 of 74 template pages and 77 on 41 of the 176 guide pages, at a 390x844 viewport, plus 4 at 1440x900. They were reachable in practice only after a client-side sweep ran, which on /tools/payslip-api in WebKit landed 360-897ms after navigation and never at all with scripting off. The tab stop, an accessible name and role=group now come from the server-rendered markup (scrollRegionProps and CodeBlock in src/components/a11y/scroll-region.tsx, and the `pre` override in src/app/guides/[slug]/page.tsx), and the same sweep now measures 0 unreachable regions at both widths. tests/scroll-keyboard.spec.ts holds it there across 111 routes at both viewports, with scripting off.

SC 2.1.2 (Level A)Conformant

No Keyboard Trap

One published surface traps focus: the site-search dialog in src/components/SiteSearch.tsx (role="dialog" aria-modal="true"), which uses useFocusTrap from src/lib/use-focus-trap.ts. That hook dispatches a CustomEvent('close') on the node when Escape is pressed (use-focus-trap.ts line 69), and SiteSearch closes on it, so the trap always has an exit. No other mounted component confines Tab focus; src/components/ExitIntentCapture.tsx is deliberately non-modal and does not trap.

SC 2.1.4 (Level A)Not applicable

Character Key Shortcuts

PayslipIQ implements no single-character keyboard shortcuts. All keyboard interactions use modifier keys or the standard tab/enter/escape vocabulary.

SC 2.2.1 (Level A)Not applicable

Timing Adjustable

There are no time limits on form submission, session timeout or content interaction.

SC 2.2.2 (Level A)Conformant

Pause, Stop, Hide

Nothing on the site blinks, scrolls or auto-updates its content. The decorative motion is the fade-in / fade-in-up / fade-in-down family defined in tailwind.config.ts, each 400-600ms and each ending in `forwards`, so it runs once and stops. Four looping animations do exist and all four are progress indicators shown only while a request is in flight: the spinners in src/components/SiteSearch.tsx, src/components/AnalysisLoading.tsx and src/components/forms/ContactForm.tsx, and the pulsing dot in AnalysisLoading. SC 2.2.2 does not require a pause control on an indicator of an activity the user has just started and which ends on its own; the search spinner additionally carries motion-reduce:animate-none. A global @media (prefers-reduced-motion: reduce) block in src/app/globals.css sets animation-duration and transition-duration to 0.001ms with animation-iteration-count 1 on *, *::before and *::after, so every animation on the site, looping ones included, stops for users who ask for reduced motion.

SC 2.3.1 (Level A)Conformant

Three Flashes or Below Threshold

No animation flashes more than three times per second. Motion is limited to one-shot fade and translate transitions.

SC 2.4.1 (Level A)Conformant

Bypass Blocks

The site-wide "Skip to main content" link is the first element in <body> in src/app/layout.tsx and targets <main id="main-content" tabIndex={-1}>, which wraps the page content and sits after the header. It is hidden with Tailwind's sr-only and revealed on focus with focus:not-sr-only, and it carries the shared `focusRing` token from src/components/ui/tokens.ts so it is visible at the moment it appears.

SC 2.4.2 (Level A)Conformant

Page Titled

Every route exports its own title through the Next.js Metadata API. The template in src/app/layout.tsx is `%s` with a default of "PayslipIQ - Plain-English UK & Ireland payslip checker": the template deliberately adds no suffix, so each route's title is published exactly as written and a route that sets none falls back to the default.

SC 2.4.3 (Level A)Conformant

Focus Order

DOM order drives focus order. src/components/RouteFocusReset.tsx moves focus to <main id="main-content"> on every Next.js client-side pathname change, restoring the focus-order baseline that SPA routing otherwise breaks; it skips the first paint so the page is not announced twice on load. It is mounted in <body> in src/app/layout.tsx. Verified in Chromium on 2026-09-25 against a running server: after clicking an in-app link, the navigation is confirmed client-side (no document load) and document.activeElement is the <main id="main-content"> element, with the new page's heading inside it. Confirmed over two consecutive in-app navigations.

SC 2.4.4 (Level A)Conformant

Link Purpose (In Context)

Link text describes its destination in context. Repeated anchors (e.g. "Read more") carry an aria-label or visually hidden suffix where the surrounding text is insufficient.

SC 2.4.5 (Level AA)Conformant

Multiple Ways

Pages are reachable four ways: the header menus and the footer link columns, both built from the shared source in src/components/navigation/nav-data.ts; the /sitemap-html index (src/app/sitemap-html), which the footer links; breadcrumb trails on category and detail pages; and site search, whose SearchAction is declared in the WebSite node built by buildIdentityGraph() in src/lib/schema-graph.ts and injected on every page by src/app/layout.tsx.

SC 2.4.6 (Level AA)Conformant

Headings and Labels

Headings and labels describe their topic or purpose, and form labels do so without relying on placeholder text. Measured 2026-09-26 in Chromium across 18 pages spanning both surfaces: every one has exactly one h1, and axe-core 4.11.4 reports no label or heading violation under the wcag2a/wcag2aa/wcag21a/wcag21aa/wcag22aa rule sets. The one page that skipped a level - /contact went h1 straight to h3, because its three sidebar cards were h3 while the form card beside them was h2 and the sidebar comes first in the DOM - is fixed: all three are h2, which is also what they are, and nothing moved on screen because Tailwind preflight resets heading font-size to inherit and src/app/globals.css defines no bare h2 or h3 rule. Re-measured the same day over 77 routes: 77 pages, 77 single h1s, and the outline is contiguous on 69 of them. The 8 that still skip a level are the calculator result panels - /calculators/pension, /calculators/student-loan, /calculators/income-tax, /calculators/national-insurance and /ie/calculators/paye, /usc, /prsi, /pension - each of which goes h1 straight to an h3 breakdown heading. axe does not report these: heading-order is a best-practice rule, not a wcag2a/aa one, and W3C is explicit that skipping a level is not in itself a failure of 1.3.1 or of this criterion. They are recorded here rather than claimed clean.

  • /contact skips from h1 to h3 (measured 2026-09-26). No other page of the 18 checked skips a level.
SC 2.4.7 (Level AA)Conformant

Focus Visible

There is no single global :focus-visible selector; the indicator is delivered by two mechanisms and both were counted. In src/app/globals.css, nine selectors declare `outline: 3px solid var(--focus)` with an offset (.btn-primary, .btn-secondary, .pq-input:focus, .pq-nav-row, .pq-nav-sub, .pq-nav-panel-link, .pq-footer-link, .pq-btn, .pq-guide-row); two more inherit that outline and only override its colour to white on dark surfaces; scrolling regions (.overflow-x-auto, .overflow-auto, [data-scroll-region]) moved on 2026-09-28 from that single-colour outline to the same two-tone `0 0 0 2px #fff, 0 0 0 5px var(--focus)` pair the generic rule uses, because they are the one focusable family that sits on the dark bg-slate-950 shell as well as on white: measured from painted pixels in Chromium 1194 at 390x844, the ring is 3.76:1 (blue) and 20.17:1 (white halo) against the #020617 shell on /tools/payslip-api, 5.36:1 against #FFFFFF on a prose <pre>, and 5.12:1 against the #F8FAFC of a guide table wrapper, against SC 1.4.11's 3:1; and three nav selectors draw an equivalent 2px double box-shadow ring instead. Elsewhere, components use Tailwind focus-visible utilities - focus-visible:ring-2 in 35 files, the shared 3px `focusRing` token from src/components/ui/tokens.ts in the rest, 44 files in total. Exactly two rules set outline:none (.pq-guide-link, .pq-hub-link): both are stretched-link overlays whose focus is drawn by the parent card's :focus-within ring (.pq-guide-card, .pq-hub-card), so no element loses its indicator.

SC 2.4.11 (Level AA)Conformant

Focus Not Obscured (Minimum)

Two pieces of author content can pin themselves over the page: the header (`sticky top-0 z-40` in src/components/layout/Header.tsx, 57px tall on mobile and 65px from md) and the cookie-consent bar (`fixed inset-x-0 bottom-0 z-50` in src/components/CookieConsent.tsx, measured at 61px / 93px / 129px at 1280 / 390 / 320px wide). src/app/globals.css now clears both globally: `html { scroll-padding-top: 5rem }` unconditionally, and `html[data-consent-open] { scroll-padding-bottom: calc(var(--pq-consent-h, 0px) + 1rem) }` while the bar is open, using the footprint CookieConsent itself publishes, so the clearance tracks the real height at every viewport. Per-element scroll-mt-24 / scroll-mt-20 remains on 28 files and is now a belt to that rule's braces. This entry was partial until 2026-09-26, and the global rule it used to claim genuinely did not exist; it exists now and was measured before this status changed. Method: from the last focusable element of a page, Shift+Tab backwards through every stop, wait for the scroll to settle (src/app/globals.css sets scroll-behavior: smooth, so an immediate read is meaningless), then sample a 3x3 grid over the focused element's rect with document.elementFromPoint and count a stop as entirely obscured when no sample returns the element. Chromium 1194 at 1280x720, before -> after: / 7 -> 0, /check 3 -> 1, /accessibility 1 -> 0, /ie/accessibility 2 -> 0. Tabbing forward on /check with the consent bar open: 6 -> 0. The one remaining /check stop is the sr-only <input type="file"> inside the dropzone label, which is 1x1 by its own styling rather than covered by author content, and whose focus ring is drawn on the label by has-[:focus-visible]:outline. /partners, which carries a third sticky bar of its own (`sticky top-0 z-30` in src/components/partners/PartnersMaster.tsx), measured 0 obscured stops in the same sweep. The elements once named here, MobileBottomNav and StickyMobileCTA, are respectively unmounted and deleted.

SC 2.5.1 (Level A)Conformant

Pointer Gestures

No multipoint or path-based gestures are required. Sliders use single-pointer drag with native keyboard equivalents.

SC 2.5.2 (Level A)Conformant

Pointer Cancellation

All actions trigger on pointerup/click rather than pointerdown, so users can drag away to abort. No down-event handlers commit destructive actions.

SC 2.5.3 (Level A)Conformant

Label in Name

Where an aria-label is set, it begins with or matches the visible label text (e.g. submit buttons read "Check my payslip" both visually and to assistive technology).

SC 2.5.4 (Level A)Not applicable

Motion Actuation

No feature is operated by device motion, tilt or shake.

SC 2.5.7 (Level AA)Conformant

Dragging Movements

The only draggable surfaces are native HTML <input type="range"> controls, on four pages: /tax-code-checker, /ie/calculators/pension, /tools/mileage-tax-rebate-calculator and /tools/marriage-allowance-checker. A native range input is operable with the arrow keys, which is the single-pointer / no-drag alternative the criterion requires. No custom drag handler exists anywhere in the codebase.

SC 2.5.8 (Level AA)Partial

Target Size (Minimum)

Primary CTAs and form inputs are sized at or above the 24x24 CSS pixel minimum, and touch-layout controls at 44x44 (see the TARGETS note in src/components/ui/tokens.ts). The footer's ~70 links were the last measured failure - they had dropped to 32px from the md breakpoint, giving unobscured targets as small as 38.1x14.5px - and were raised to a 40px minimum on 2026-09-24; the .pq-footer-link rule in src/app/globals.css and the note in src/components/layout/Footer.tsx record it. axe's target-size rule runs over the six anchor pages in tests/a11y.spec.ts and passes.

  • Target size is verified automatically on six anchor pages in CI. A 2026-09-26 sweep ran axe's wcag22aa rules over 14 further pages (/accessibility, /ie/accessibility, /ie/check/manual, /check/manual, /authors, /trust, /trust/correction-policy, /trust/editorial-policy, /partners, /sitemap-html, /tax-codes/compare, /contact, /glossary and a guide) and found no target-size violation on any of them, which is 20 pages of roughly two thousand routes. On the remaining templates this still rests on the design-system defaults and the manual pass, not on a measurement.
  • OPEN, found later on 2026-09-26 by widening that sweep to 77 routes: /tax-codes carries two inline links that measure 222x16 CSS pixels against the 24x24 minimum - the tax-code card link (text-green-600, href /tax-codes/1257L) and "What does 1257L mean?" (text-green-700, href /ask/what-does-1257l-mean). axe target-size, serious, 2 nodes. The 14-page set did not include /tax-codes, so it reported clean. Not fixed: the remedy is vertical padding or line-height on those two links in src/app/tax-codes/page.tsx, a visual change to a hub page, and it is the reason this criterion is still partial rather than a claim.

Remediation target: 2026-12.

Understandable

Users can understand the information and the operation of the interface.

SC 3.1.1 (Level A)Conformant

Language of Page

src/app/layout.tsx sets <html lang="en-GB"> on every page; the value is a static literal and is not derived from the request. Irish routes correct it two ways: src/app/ie/layout.tsx wraps their content in <div lang="en-IE">, which is in the server-rendered HTML, and it also mounts src/app/ie/HtmlLangFix.tsx, a client component whose effect sets document.documentElement.lang to "en-IE" after hydration. The criterion is met either way because the human language of every page is English and is programmatically determined; what the regional variant on <html> depends on is JavaScript, so before hydration an /ie page reports en-GB rather than en-IE.

SC 3.1.2 (Level AA)Conformant

Language of Parts

The site publishes no passages in a language other than English, so there is nothing that requires a lang attribute of its own. The one language-of-parts declaration that exists is the en-IE wrapper on Irish routes (src/app/ie/layout.tsx). Irish statutory vocabulary on the /ie surface (PRSI, USC, Revenue) appears as English-language proper nouns and acronyms, which the criterion does not require to be marked up.

SC 3.2.1 (Level A)Conformant

On Focus

No element triggers a context change on focus. Form fields validate on blur or submit, never on focus.

SC 3.2.2 (Level A)Conformant

On Input

No input element auto-submits or navigates. Submission is gated on an explicit button.

SC 3.2.3 (Level AA)Conformant

Consistent Navigation

src/components/layout/Header.tsx and src/components/layout/Footer.tsx are mounted by src/app/layout.tsx for every route. Both take their links from one source - HEADER_MENUS and FOOTER_COLUMNS in src/components/navigation/nav-data.ts - so the order cannot drift between the two or between pages. src/components/MobileBottomNav.tsx, named here previously, was imported by no file and rendered on no page; it was deleted on 2026-09-26, so no second navigation pattern exists to be inconsistent with.

SC 3.2.4 (Level AA)Conformant

Consistent Identification

Repeated UI primitives (buttons, badges, callouts) share a single Tailwind component class and identical accessible name across every surface.

SC 3.2.6 (Level A)Conformant

Consistent Help

src/components/layout/Footer.tsx is mounted by the root layout on every route and renders FOOTER_COLUMNS from src/components/navigation/nav-data.ts. The "Company" column carries { href: '/contact', label: 'Contact' } in the same relative order on every page, which is the consistent-help mechanism WCAG 2.2 asks for. (The file cited here previously, src/components/Footer.tsx, was deleted as dead on 2026-09-25; the live footer is the layout/ one.)

SC 3.3.1 (Level A)Conformant

Error Identification

Validation errors are identified in text, linked to the offending input via aria-describedby, and surfaced through role="alert" wrappers in the form components.

SC 3.3.2 (Level A)Conformant

Labels or Instructions

Every input carries a programmatic <label htmlFor>, and complex fields (e.g. tax code) include helper text describing the expected format.

SC 3.3.3 (Level AA)Conformant

Error Suggestion

Zod-driven validators in PayslipForm.tsx and IePayslipForm.tsx produce specific error copy ("must be a positive number", "tax code must match HMRC format") rather than a generic "invalid".

SC 3.3.4 (Level AA)Not applicable

Error Prevention (Legal, Financial, Data)

PayslipIQ submits no legal, financial or data-modifying transactions on the user's behalf. All flows are read-only analyses; no irreversible action is taken without explicit confirmation, and no user data is persisted server-side.

SC 3.3.7 (Level A)Conformant

Redundant Entry

No multi-step flow asks the user to retype information already provided in the same session. The /check single-page form collects all inputs in one pass.

SC 3.3.8 (Level AA)Not applicable

Accessible Authentication (Minimum)

PayslipIQ does not require user authentication. There are no passwords, MFA challenges or cognitive function tests on any public route.

Robust

Content is robust enough to be reliably interpreted by a wide range of user agents, including assistive technology.

SC 4.1.2 (Level A)Conformant

Name, Role, Value

Custom components delegate to native HTML elements (button, input, a) so role and value come for free. ARIA is added only where semantic HTML is insufficient: the role="status" / aria-live regions in src/components/PayslipForm.tsx, src/components/PayslipUpload.tsx and src/components/IePayslipForm.tsx, aria-describedby and aria-invalid on form errors, and aria-current on the final breadcrumb (src/components/ui/Breadcrumb.tsx, src/components/Breadcrumbs.tsx, src/components/programmatic/CityCodeBreadcrumb.tsx). This entry cited "aria-current on the active nav link" until 2026-09-26, when the sweep found that no header or footer navigation link carried it - the only nav that did was src/components/MobileBottomNav.tsx, which is imported by no file - and the line was corrected to say so. The gap itself was closed the same day and the evidence is rewritten here to what is now true: src/components/navigation/NavLink.tsx sets aria-current="page" when its href equals usePathname(), and every link in the desktop menus, the mobile menu and the footer columns is one (src/components/navigation/DesktopNav.tsx, MobileNav.tsx, src/components/layout/Footer.tsx), as is the header wordmark, which is the only link to the home page outside a page body. It is baked into the prerendered HTML rather than applied after hydration, because usePathname() renders on the server too: .next/server/app/pricing.html ships aria-current on exactly three links. Verified in Chromium 1194 against a production build on 12 routes. /pricing, /business, /ie, /tools, /guides, /check and /calculators/take-home each carry exactly one aria-current="page" in each of the three navigation regions - nav "Primary" (36 links), nav "Primary (mobile)" (37) and nav "Footer" (59) - and in every case it is the page being viewed. /contact, /tax-codes and /accessibility carry one in the footer and none in the header, correctly, because the header navigation does not link them. One route is marked twice per header region: /payslip-checker/jobs appears in nav-data.ts as both the Pay scales hub ("Payslip checks by job") and a Tools link ("Payslip checker by job"), so both are marked; both are accurate, and collapsing the two labels is an information-architecture decision rather than an ARIA one. The /check call to action at the foot of the mobile panel is deliberately left unmarked - it is an action, not a place in the navigation. axe-core 4.11.4 reports no name/role/value violation on any of the 77 routes swept on 2026-09-26.

SC 4.1.3 (Level AA)Conformant

Status Messages

Every state transition in a checker flow is announced without moving focus. On the UK surface src/components/PayslipForm.tsx wraps its loading and result containers in role="status" aria-live="polite" aria-busy, and src/components/PayslipUpload.tsx does the same for the photo-upload flow with an additional role="alert" aria-live="assertive" container for errors. src/components/IePayslipForm.tsx had neither, which is why this entry was partial until 2026-09-26; it now renders a persistent role="status" aria-live="polite" element declared before its three state branches and returned as the first child of each, so React reconciles it to one DOM node and its text changes rather than the node being replaced - the case a live region is actually guaranteed to announce. Its submit-failure banner gained role="alert" aria-live="assertive" in the same change. Measured in Chromium 1194 against a production build with a MutationObserver bound to the region before submit: one node throughout (identity confirmed by a marker attribute), aria-busy false -> true -> false, text "" -> "Analysing your payslip. This usually takes 10 to 20 seconds." -> "Your payslip analysis is ready. Your results are shown below.", four mutations across the two transitions. The error path was exercised separately: a client-validation rejection and a 429 from /api/ie/analyse both surfaced role="alert" aria-live="assertive" carrying the message text.

Known limitations and remediation

The following items are tracked separately from the per-criterion evidence above. Where a remediation date is committed it is stated; where no date has been agreed the item says that rather than carrying one that was invented to fill the gap.

  1. Open Graph image alt text is a single fixed string set on the root layout, not authored per route. It is read by social and search crawlers rather than by assistive technology on this site, so the user-facing impact is low; it is listed here because SC 1.1.1 is claimed as partial and this is why. No date is attached because none has been agreed.
  2. Target size is measured on six pages, not on all of them. axe’s target-size rule runs over the six anchor pages on every pull request and passes. The site publishes roughly two thousand routes; on the rest, the 24x24 minimum rests on the design-system defaults recorded in src/components/ui/tokens.ts and on the manual pass, not on a measurement. The last measured failure was the footer link list, which had dropped to 32px from the md breakpoint and was raised to a 40px minimum on 2026-09-24. A seventy-seven-route sweep on 2026-09-26 found the next one, on /tax-codes; it is listed separately below.
  3. Wide comparison tables on/tax-codes/compare scroll horizontally below 480px viewport width. This is permitted by the data-table exception in SC 1.4.10, so it is a quality item rather than a conformance gap; a vertical stacked layout for narrow viewports is on the backlog and carries no committed date.
  4. PDF guides are forthcoming. Every one will be tagged-PDF (PDF/UA-compliant) and accessibility-checked against the Matterhorn Protocol before publication, and this statement will name where they live once they have somewhere to live. No PDFs are currently published, so no remediation gap exists today.
  5. Calculator results pages provide a tabular alternative to the visual breakdown. An audio summary is not provided — this is out of scope for the current product type and is not on the roadmap.
  6. Two links on one page are below the minimum target size. Widening the automated sweep from twenty pages to seventy-seven on 26 September 2026 found two inline links on /tax-codes that render 222 by 16 CSS pixels against a 24 by 24 minimum: the link out of each tax-code card, and “What does 1257L mean?”. This is SC 2.5.8, which is recorded as partial above and was already partial for a coverage reason; this is now a measured failure rather than an unmeasured surface. No date is attached because none has been agreed.
  7. Welsh-language version — we do not yet offer a Welsh-language interface. We are evaluating this for 2027 in line with the Welsh Language (Wales) Measure 2011 posture.

Testing methodology

We are committed to honesty about the testing posture supporting this statement. Our current cadence - most recent manual pass 2026-08-04 - is as follows.

Automated, every pull request (CI)

  • axe-core via Playwright over the six anchor pages (home, /check, /tax-code-checker, /calculators/take-home, /pricing, /ie), run against the pull request’s own preview deployment. A single WCAG 2.2 AA violation fails the check and the pull request goes red — seetests/a11y.spec.tsand thea11yjob in.github/workflows/test.yml. The run is aborted rather than pointed at the live site if no preview URL resolves.
  • Lighthouse CI with the Accessibility category asserted at ≥ 95 as a hard error, on the same six anchor pages and on /glossary (lighthouserc.js, .github/workflows/lighthouse-ci.yml). This also runs on pull requests only.
  • ESLint on every pull request, with the accessibility rules that ship inside next/core-web-vitals: image alt text, valid ARIA attribute names and value types, ARIA on elements that do not support it, and the ARIA a declared role requires or permits. They are reported as warnings, so they do not by themselves fail the check. Nothing in our static analysis inspects focus order, label association or prefers-reduced-motion handling; those are covered by the axe run above and the manual pass below, and we do not claim otherwise.

The full browser suite — smoke, API, consent, search and the same accessibility spec — is run again against each production deployment by .github/workflows/e2e-after-deploy.yml. That run happens after the code is live. It is a second net, not a pre-merge gate.

Scheduled manual pass - every six months

The most recent manual pass completed on 2026-08-04: an axe-assisted WCAG 2.2 review of all six anchor pages, with the issues found (keyboard access to the file upload, form labelling and ARIA on the checker forms, contrast, and text equivalents for colour-only states) fixed and shipped the same day. The next manual pass is planned for 2027-02-04. The assistive-technology combinations we test against are listed below; please do not assume that every combination was individually exercised in the most recent pass.

  • NVDA 2024.x with Firefox and Chrome on Windows 11.
  • VoiceOver with Safari on macOS (current public release).
  • TalkBack with Chrome on Android (current public release).
  • VoiceOver with Safari on iOS (current public release).
  • Dragon NaturallySpeaking 16 with Chrome on Windows 11.
  • Windows High Contrast and macOS Increase Contrast smoke checks on every navigational and form surface.

If you use an assistive technology not on this list and encounter a barrier, please tell us - we will reproduce on the combination you are using and respond per the timelines below.

AAA aspirations

We do not claim WCAG 2.2 Level AAA conformance. We do, however, design towards several AAA criteria where they intersect with the comprehension needs of a payroll audience: SC 1.4.6 Contrast (Enhanced) is met for body text on most surfaces; the 3px focus outline clears the 2 CSS pixel minimum thickness SC 2.4.13 Focus Appearance asks for, though we have not measured the adjacent-colour contrast that criterion also requires and so do not claim it; and SC 3.1.5 Reading Level is supported by plain-English glossaries on every guide. SC 3.3.9 Accessible Authentication (Enhanced) is not applicable because no authentication exists.

Reporting accessibility issues

If you encounter a barrier on PayslipIQ, please tell us. You can:

We commit to acknowledging accessibility reports within 5 working days and providing a substantive remediation timeline within 14 days of the initial acknowledgement. Where a fix is straightforward, it will normally ship inside the same fortnight.

Enforcement and regulatory framework (UK)

PayslipIQ is operated as a private-sector consumer service in the United Kingdom. Our accessibility obligations arise principally under:

If you are not satisfied with our response, you can contact the Equality and Human Rights Commission (EHRC) who enforce the Equality Act 2010 in England, Scotland and Wales. In Northern Ireland, contact the Equality Commission for Northern Ireland.

Preparation of this statement

This statement was prepared on 2026-05-05 following an internal accessibility audit covering keyboard operation, screen-reader semantics, colour contrast, focus order under SPA navigation, focus visibility under sticky regions, form label association and live-region announcement of asynchronous state. The audit method combined automated scanning (axe-core, Lighthouse) with manual assistive-technology testing as listed above.

The statement text was last changed on 2026-09-26. The most recent manual accessibility pass was earlier, on 2026-08-04; the 2026-09-26 revision was a desk review that re-opened every code citation on this page against the repository and corrected the ones that no longer held, rather than a new test of the site. It is reviewed at least every six months; the next scheduled manual pass is 2027-02-04. It is also re-reviewed whenever a substantive change is made to the navigation, forms or design system.