PayslipIQ Ireland Accessibility Statement
Statement prepared on 2026-05-05; text last updated 2026-09-26. Last assistive-technology regression test: 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.
We additionally align with EN 301 549 v3.2.1, the harmonised European standard for ICT accessibility that incorporates WCAG 2.1 AA by reference and forms the technical basis for compliance with the EU Web Accessibility Directive (2016/2102) and the European Accessibility Act (Directive 2019/882). WCAG 2.2 AA is a strict superset of WCAG 2.1 AA, so meeting the WCAG 2.2 AA target satisfies the EN 301 549 baseline for the criteria covered by this statement.
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 and law
- WCAG 2.2 — W3C Recommendation, October 2023. The technical basis for our per-criterion claims.
- EN 301 549 v3.2.1 — the European harmonised standard for ICT accessibility. Section 9 of EN 301 549 incorporates WCAG 2.1 Level AA by reference for web content, and we exceed that floor by targeting WCAG 2.2 AA.
- Web Content Accessibility Directive 2016/2102, transposed in Ireland by S.I. 358/2020 European Union (Accessibility of Websites and Mobile Applications of Public Sector Bodies) Regulations 2020. PayslipIQ is not a public sector body, but we voluntarily adopt the directive’s statement template because it is the standard EU model.
- European Accessibility Act — Directive (EU) 2019/882, transposed in Ireland by the European Union (Accessibility Requirements of Products and Services) Regulations 2023 and enforceable from 28 June 2025. We treat our consumer-facing checker as in scope of the e-commerce services category.
- Disability Act 2005 (Ireland), section 28 — informational. The statutory IT-accessibility duty applies to public bodies; we adopt its ‘accessible by default’ principle voluntarily.
The UK-side statement at /accessibility is the canonical record of our technical conformance and contains the same per-criterion evidence.
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, UI borders and focus rings. Every pairing is measured by hand and the measured ratio is recorded beside the token it belongs to in
src/components/ui/tokens.ts. No build step re-measures them; what runs automatically is axe’s colour-contrast rule, which from 26 September 2026 runs over twenty-three pages on every pull request rather than six — the six anchor pages plus one page per template, including/ie/calculators/pension, which failed that day and now does not. - Every form input on the Irish payslip checker is paired with a programmatically associated label; error messages are linked through
aria-describedbyand announced viarole=“alert”. - One region is pinned to the viewport: the header (
sticky top-0insrc/components/layout/Header.tsx), and the cookie-consent bar is pinned to the bottom edge while it is open. Anything the browser scrolls to clears both, becausesrc/app/globals.cssdeclareshtml { scroll-padding-top: 5rem }and ascroll-padding-bottomcomputed from the consent bar’s own measured height. That covers keyboard navigation, not only in-page links: before the rule existed, Shift+Tabbing backwards up this page left two controls entirely underneath the header, and afterwards none (Chromium, 1280×720, 26 September 2026). The per-elementpq-anchor-offsetandpq-anchor-offset-smclasses on 28 templates remain as a second layer (SC 2.4.11, recorded as conformant below). - Focus is visible on interactive elements by two mechanisms, not one: ten selectors in
src/app/globals.cssdraw a 3px:focus-visibleoutline with an offset, and components elsewhere use Tailwind’sfocus-visible:ring-2utility or the shared 3px token insrc/components/ui/tokens.ts— 44 files in total. There is no single selector covering the whole site. - Keyboard and screen-reader focus is moved on client-side route changes.
src/components/RouteFocusReset.tsxis mounted in the root layout and moves focus to<main id="main-content">on every pathname change (SC 2.4.3), and was verified in Chromium on that date by following an in-app link and reading back the focused element. It was mounted on 2026-09-25; before that it was imported by no file and did not run, and this page said it did. - Semantic HTML5 first; ARIA is added only where semantic HTML is insufficient.
- Motion respects
prefers-reduced-motionand 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).
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
- Open Graph image alt text is a single fixed string set on the root layout, not authored per route. No date is attached because none has been agreed.
- Target size is measured on six pages, not on all of them. axe’s
target-sizerule runs over the six anchor pages (one of which is the Irish home page) on every pull request and passes. On the remaining templates the 24x24 minimum rests on the design-system defaults and the manual pass, not on a measurement. - Wide data tables on the Irish methodology pages (
/ie/methodology/prsi,/ie/methodology/usc) and the PAYE, PRSI, USC and take-home calculators scroll horizontally below 480px viewport width. This is permitted by the data-table exception in SC 1.4.10; a stacked narrow-viewport layout is on the backlog and carries no committed date. The scrollable regions are keyboard-reachable — the tab stop, the accessible name androle="group"come fromsrc/components/a11y/scroll-region.tsxin the HTML the server sends. - 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— a UK-surface page reachable from here — that render 222 by 16 CSS pixels against a 24 by 24 minimum. This is SC 2.5.8, recorded as partial above. No date is attached because none has been agreed. - PDF guides are forthcoming. Every one will be tagged-PDF (PDF/UA-compliant) and Matterhorn-checked before publication, and this statement will name where they live once they have somewhere to live.
- Calculator results provide a tabular alternative to the visual breakdown. An audio summary is not provided; this is out of scope for the product type.
- Irish-language interface — the site is currently English-only (en-IE). A Gaeilge interface is on the longer-term roadmap and is not committed for 2026.
Assistive technologies tested
The most recent manual pass completed on 2026-08-04. The combinations below are the ones we test against; please do not assume that every combination was individually exercised in the most recent pass. If you use an assistive technology that is not on this list and hit a barrier, tell us and we will reproduce on the combination you are using.
- NVDA 2024.4 with Firefox 132 and Chrome 132 on Windows 11.
- VoiceOver with Safari 18 on macOS Sequoia 15.x.
- TalkBack on Android 15 with Chrome 132.
- VoiceOver with Safari on iOS 18.
- Dragon NaturallySpeaking 16 with Chrome 132 on Windows 11.
- Windows High Contrast and macOS Increase Contrast smoke checks on every navigational and form surface.
Reporting accessibility issues
If you encounter a barrier on PayslipIQ, please tell us:
- Email accessibility@payslipiq.co.uk.
- Use the contact form with the subject line ‘Accessibility’.
We commit to acknowledging accessibility reports within 5 working days and providing a substantive remediation timeline within 14 days of the initial acknowledgement.
Enforcement and regulatory framework (Ireland and EU)
If you are not satisfied with our response, you can escalate as follows:
- For matters falling under the Equal Status Acts 2000–2018, contact the Workplace Relations Commission.
- For matters under S.I. 358/2020, the National Disability Authority (Centre for Excellence in Universal Design) is the statutory monitoring body and can be contacted at nda.ie.
- For matters arising under the European Accessibility Act, the Competition and Consumer Protection Commission (CCPC) is the designated market-surveillance authority for consumer services in Ireland.
- EU residents elsewhere may refer concerns to the relevant national market-surveillance authority designated under the European Accessibility Act in their member state.
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. Automated scanning (axe-core, Lighthouse) was combined with manual assistive-technology testing.
The statement is reviewed at least every six months, and again whenever a substantive change is made to the navigation, forms or design system. The text was last changed on 2026-09-26; the next scheduled manual pass is 2027-02-04.
Read more about how we handle data on our trust page.