Ditto Care — Accessibility Audit & European Accessibility Act Assessment

Target: https://www.dittocare.com and all discoverable sub-pages (EN + nl-NL)
Prepared for: Ditto Care
Audit date: 14 September 2026
Benchmark: WCAG 2.1 Level AA (also tested against WCAG 2.2 AA) — the technical baseline referenced by EN 301 549 and therefore by Directive (EU) 2019/882 (the European Accessibility Act, "EAA")
Method: Automated + manual, rendered-DOM testing (not source-only)


1. Executive summary

Ditto's public website is a well-built, fast Framer site with several accessibility fundamentals already right. Automated testing across 58 URLs / 116 rendered page loads (desktop + mobile) found no page that is wholly unusable, and a number of areas pass cleanly:

Against that, the audit found 14 distinct WCAG violations, 1,679 failing element instances, and — more importantly — four patterns that break real user journeys for disabled users. The most consequential are:

#PatternSeverityWCAGUser impact
B1Language switcher changes content but not <html lang>Blocker3.1.1 (A)A Dutch screen-reader user who switches language hears Dutch read by an English voice — effectively unintelligible. Only a full page reload fixes it.
B2Cookie consent banner traps keyboard focus while page content stays mouse-reachableBlocker2.1.1 (A), 2.4.3 (A)A keyboard-only user cannot reach any page content until they dismiss the banner — mouse users can.
B3Mobile menu button is a <div> with no role, no accessible name, no expanded stateBlocker4.1.2 (A)Screen-reader users hear an unnamed "group" and cannot tell it is a menu or whether it is open. (It is keyboard-operable once the cookie banner is dismissed — verified.)
B4/support category tabs use role="tab" with aria-pressed and no tablist parentBlocker1.3.1, 4.1.2 (A)Six FAQ categories announce incorrectly; selected state is not conveyed.

Plus one high-volume, low-effort cluster: 9 distinct colour-contrast pairs fail across 27 of 58 URLs, and 41 of 58 URLs have touch targets below the 24×24 px WCAG 2.2 minimum. These are palette/design-token fixes with outsized return.

Overall conformance position: the site does not currently conform to WCAG 2.1 AA. It is close in structure and far in detail — the failures are concentrated in a small number of repeated component patterns rather than being systemic to every page.


2. Scope and method

2.1 What was tested

All 58 URLs discovered from sitemap.xml (EN and nl-NL sitemaps), plus 4 pages linked from the homepage but absent from the sitemap/legal/eu-ai-act, /legal/privacy-policy, /legal/terms-conditions, /news, and the /legal/* family. All 58 returned HTTP 200 and all 58 had distinct content (no duplicate-page inflation).

Page families: homepage, product/feature pages, /professionals + /hcp + /hcp-tobi-test (clinician journey), /about, /press, /news, /support, /together, /download-the-app, /app-download, /ylvc, /cclg, 5 waitlist/thank-you pages, 5 articles, 3 legal pages, /master-components, and /dach.

Out of scope (flagged, not tested): the Ditto iOS/Android app itself (linked via dittocare.go.link), and third-party destinations (ditto.homerun.co, Typeform, Google Drive, PubMed, app stores). The app is in scope for the EAA as a "mobile device-based service" in its own right and should be audited separately (§7.3).

2.2 How it was tested

Automated scanning alone is insufficient for a compliance audit, so three layers were used:

  1. Rendered-DOM sweep — Chromium (headless, 1440×900 desktop / 390×844 mobile) with axe-core at wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22aa + best-practice, after scrolling each page fully to force Framer's lazy-loaded content into the DOM. 116 page loads, 0 errors. Raw results: axe-raw.json.
  2. Source-HTML analysis — a second pass over the static HTML (static-report.json) to catch things a rendered scan can miss and to cross-check rendered counts against build output.
  3. Behavioural journey testing — scripted keyboard traversal, focus-trap detection, live-region and aria-invalid inspection, form submission, language switching, 320 px reflow, 200% zoom, WCAG 1.4.12 text-spacing, and reduced-motion emulation (journeys.json).

Reproducibility: the harness, wrapper and raw outputs are retained alongside this report (axe-audit.mjs, analyze.mjs, journeys.mjs, rollup.mjs, detail.mjs, plus shots/).

A note on false positives. Two findings were investigated and withdrawn after deeper testing, and are recorded here for transparency:

2.3 Confirmed environment limitation

Testing ran on a Linux container without root. Chromium and its font stack were provisioned manually; all findings were produced by a real browser with real layout, real computed styles and real focus behaviour. One caveat: no licensed screen reader (NVDA/JAWS/VoiceOver) was available, so screen-reader findings are derived from the accessibility tree (roles, names, states, aria-*) rather than from listening to output. Findings B1, B3 and B4 are accessibility-tree defects that do not depend on screen-reader nuance; a scripted NVDA pass is nonetheless recommended to confirm wording and announcement order (§8).


3. Does the EAA apply to Ditto?

This determines what the findings below mean commercially, so it is worth stating precisely.

3.1 The legal mechanics

The EAA requires in-scope products and services to meet the accessibility requirements in Annex I. For services, the operative requirement is Annex I, Section III:

"(c) making websites, including the related online applications, and mobile device-based services, including mobile applications, accessible in a consistent and adequate way by making them perceivable, operable, understandable and robust"

"Perceivable, operable, understandable and robust" is the WCAG POUR framework; EN 301 549 is the harmonised standard that gives this presumption of conformity, and it points at WCAG 2.1 Level AA. Article 4(1) requires economic operators to only provide conforming services. The deadline was 28 June 2025, implemented in the Netherlands by the Implementatiewet toegankelijkheidsvoorschriften producten en diensten.

3.2 Healthcare is not listed — but e-commerce catches it

Healthcare is not a separate category in EAA Article 2. However, the EAA covers "e-commerce services", defined as services provided "at a distance, through websites and mobile device-based services by electronic means and at the individual request of a consumer with a view to concluding a consumer contract." Legal analysis of the EAA's application to medical devices is explicit that:

"Commercial web or app-based health services which are medical devices provided directly to consumers must be EAA-compliant" and "Features and functionality of web or app-based health services provided to consumers on a subscription or other payment basis on a device may also be captured by the EAA as an 'e-commerce service'."

Ditto is a consumer health app distributed directly to consumers, marketed on a public website, with app-store download funnels and an in-app subscription model. On that basis the website and app are, in our assessment, within the EAA's e-commerce scope, and Annex I Section III(c) applies to this website.

E-commerce services also attract a specific requirement (Annex I, Section IV(g)): identification, security and payment functionality must be perceivable, operable, understandable and robust.

3.3 The exemption that could change this — and the one question Ditto must answer

EAA Article 4(5) states:

"Microenterprises providing services shall be exempt from complying with the accessibility requirements referred to in paragraph 3 of this Article and any obligations relating to the compliance with those requirements."

A microenterprise is fewer than 10 employees AND (annual turnover ≤ €2 million or balance sheet total ≤ €2 million). This exemption applies to services (not to products).

This audit cannot resolve whether Ditto qualifies, and it is the single most important scoping question for the company:

Three further points materially limit the value of the exemption even if it applies:

  1. It is provider- and service-specific. If Ditto supplies its service to hospitals, insurers or public bodies — or provides it on behalf of another economic operator — the other operator's obligations are unaffected, and accessibility will be imposed contractually (via EN 301 549 in procurement) regardless of Ditto's size.
  2. It is a floor, not a ceiling. National disability-discrimination law, sector procurement rules and app-store policies apply independently.
  3. The site already publishes an EU AI Act Article 50 transparency page, indicating the company is deliberately positioning as an EU-regulated digital-health provider. Accessibility commitments sit in the same regulatory neighbourhood and are increasingly asked for in hospital/insurer due diligence.

Recommendation: confirm headcount and turnover against the microenterprise threshold before treating this as a legal question. Treat the findings below as product requirements either way — they are ordinary usability defects for a patient-facing health product whose users skew older, are unwell, or are temporarily impaired.


4. Conformance summary

WCAG 2.1 Level AA: does not conform.
**WCAG 2.2 Level AA: does not conform.

Count
URLs audited58 (29 paths × EN + nl-NL)
Rendered page loads116 (desktop + mobile)
Distinct axe rules violated14
Total failing element instances1,679
Items flagged "incomplete" needing manual review3,123 nodes across 5 rules
Pages with no skip link58 / 58
Pages with a non-semantic clickable element58 / 58

Violations by rule

RuleWCAG SCLevelURLs /58Instances
target-size2.5.8AA (2.2)4190
region (content outside landmarks)1.3.6 / best practice331,056
page-has-heading-one1.3.1A3262
heading-order1.3.1A3268
landmark-one-main1.3.1A3162
color-contrast1.4.3AA27125
aria-hidden-focus4.1.2A2688
link-name2.4.4 / 4.1.2A1422
link-in-text-block1.4.1A1250
aria-command-name4.1.2A820
label4.1.2 / 3.3.2A48
aria-allowed-attr4.1.2A212
aria-required-parent1.3.1A212
image-alt1.1.1A24

Counts are in distinct URLs (each URL counted once even if it failed on both desktop and mobile). The 58 URLs comprise 31 English and 27 Dutch pages.

Manual findings not detectable by automation

FindingWCAG SCLevel
Language switch does not update <html lang>3.1.1A
Cookie banner traps keyboard focus while content is mouse-reachable2.1.1, 2.4.3A
No autocomplete on any form field (Name, Email)1.3.5AA
Auto-playing looping video with no pause mechanism, not stopped by reduced-motion2.2.2A
No skip-to-content link on any page2.4.1A
Form errors rely solely on native browser bubbles — no aria-invalid, no live region3.3.1A

5. Findings — patterns and fixes

Ordered by user impact. Each is a repeatable pattern, so one fix resolves many instances.


B1 — Language switcher does not update the page language [Blocker]

WCAG 3.1.1 Language of Page (Level A) · EAA Annex I §III(c)

The in-page language <select> performs a client-side route change. The URL, <title>, and all visible content switch to Dutch — but document.documentElement.lang remains en. Verified reproducibly:

1. loaded EN /            lang=en     path=/      title="Ditto | Care. Clarified"
2. +2s after switch       lang=en     path=/nl/   title="Ditto | Heldere zorg voor jou en je dierbaren"
3. +8s after switch       lang=en     path=/nl/   title="Ditto | Heldere zorg voor jou en je dierbaren"   <-- still en
4. after hard reload      lang=nl-NL  path=/nl/   title="Ditto | Heldere zorg voor jou en je dierbaren"
5. switched back to EN    lang=nl-NL  path=/      title="Ditto | Care. Clarified"                        <-- still nl

Only a hard reload corrects it — and the fault is symmetric (NL→EN leaves nl-NL).

Why this matters: a Dutch screen-reader user takes exactly this journey. Their screen reader applies an English pronunciation profile to Dutch text, which makes the content unintelligible — the worst possible outcome for a product whose entire purpose is making medical information understandable. This is invisible to single-page automated scans (each page is correct in isolation), and it is the highest-severity finding in the audit.

Fix: update document.documentElement.lang on every client-side locale navigation (a useEffect on locale change, or set it in the router's navigation handler). Ditto already emits correct <link rel="alternate" hreflang> tags, so the correct value is available. Add a regression test that switches language and asserts lang.


B2 — Cookie consent banner traps keyboard focus [Blocker]

WCAG 2.1.1 Keyboard (A), 2.4.3 Focus Order (A) · EAA Annex I §III(c)

The Cookiebot consent banner (#CybotCookiebotDialog) cycles keyboard focus through four controls indefinitely:

Tab 0: A     "Show details"
Tab 1: BUTTON "Allow all"
Tab 2: BUTTON "Customize"
Tab 3: BUTTON "Deny"
Tab 4: A     "Show details"   <-- wraps; page content never reachable
...repeats forever

Critically, the banner is not visually modal: elementFromPoint(720,300) returns a normal page paragraph, the page remains scrollable, and mouse users can scroll and interact with the page behind it. So keyboard-only users are locked out of content that mouse users can freely use — an inconsistent, unequal experience.

Compounding this, the banner is exposed as role="region", not role="dialog", and has no aria-modal. It does carry a correct aria-labelledby pointing at #CybotCookiebotDialogBodyContentTitle.

Fix: the correct treatment is to make it a genuine modal dialog — role="dialog" + aria-modal="true" — so the focus trap is legitimate and announced, and then ensure the trap releases and returns focus on dismissal. Alternatively, if it is meant to be non-modal, remove the focus trap. Either is acceptable; the current hybrid is not. This is Cookiebot-supplied markup, configurable via its CMP settings, but it is Ditto's compliance obligation regardless of who authored the component — third-party embeds are part of the delivered service under Annex I §III.


B3 — Mobile menu button has no role, name or state [Blocker]

WCAG 4.1.2 Name, Role, Value (Level A) · EAA Annex I §III(c)

On viewports ≤ ~768 px the primary navigation is behind a control that is:

<div class="framer-9dxb01" data-framer-name="Menu button" tabindex="0" style="...">
  <div><svg focusable="false" ...><path d="M224,128a8,8,0,0,1-8,8H40a8..." /></svg></div>
</div>

The equivalent desktop logo link does carry aria-label="Home button", so the pattern is understood in the codebase but was not applied here.

Corrected behaviour (verified — important): with the cookie banner dismissed, Enter and Space both open the menu correctly, focus moves sensibly through the menu items, and Escape closes the menu and returns focus to the button. Keyboard operability is therefore fine; only the semantics are broken.

Fix: use a real <button type="button" aria-label="Menu" aria-expanded="false" aria-controls="site-menu"> and toggle aria-expanded on open/close. This is approximately a five-line change in the Framer component and removes a Level A failure from every mobile page.


B4 — Support-page category tabs use an invalid ARIA pattern [Blocker]

WCAG 1.3.1 (A), 4.1.2 (A) · EAA Annex I §III(c)

On /support and /nl/support, six FAQ category controls ("Features", "Account and log-in", "About Ditto", "Professionals", "Privacy", "Support") are marked up as:

<button role="tab" aria-pressed="true" tabindex="0">Features</button>

with role="presentation" on the parent <li> and no role="tablist" ancestor anywhere, and no role="tabpanel" for the content they reveal. Axe flags this as two critical failures: aria-required-parent (required ARIA parent role tablist not present) and aria-allowed-attr (aria-pressed is not allowed on role="tab").

Additionally, all six tabs have tabindex="0". The ARIA tab pattern requires roving tabindex — only the selected tab is in the tab order and arrow keys move between tabs. As built, a keyboard user must Tab through all six to leave the group, and a screen-reader user gets no announcement of which panel is active because aria-selected is never set.

Fix: either (a) complete the tab pattern — add role="tablist", use aria-selected instead of aria-pressed, add aria-controls + role="tabpanel" + aria-labelledby, and implement roving tabindex with arrow-key navigation; or (b) far simpler and recommended here, drop the ARIA entirely and use plain disclosure <button aria-expanded> elements. A disclosure pattern is semantically honest for FAQ categories and much harder to get wrong.

Note: the Cookiebot tabs on the same page are correctly built — the defect is in Ditto's own component.


S1 — Colour contrast: 9 distinct failing pairs across 27 of 58 URLs [Serious]

WCAG 1.4.3 Contrast (Minimum), Level AA · EAA Annex I §III(b)(v)

Every failure reduces to a small palette problem. Fixing these nine token pairs clears all 125 failing instances:

RatioForegroundBackgroundSizeWeightNodesPages
2.46#7aa8c9#fffbf718pxnormal22
2.91#0099ff#fffbf718px / 14pxbold62
3.18#688da6#faf2eb12pxnormal84
3.48#748a9a#fffbf716pxbold3015
3.51#748c9d#ffffff16pxbold1515
4.02#bfcdd6#2a628912pxnormal62
4.14#52728a#dceaf412pxnormal62
4.37#5b7587#faf2eb12pxnormal42

The worst offenders are brand-adjacent: #748a9a / #748c9d (a muted slate used for 16px bold body copy on /download-the-app, /app-download, /ylvc, /cclg) and #0099ff (bright blue links on /privacy). All require 4.5:1 at these sizes; none reach it.

Fix: darken the slate to ≈#5a6b78 and the link blue to ≈#0b6ba8, then re-verify. Because these are Framer design tokens, the change propagates site-wide from a handful of edits.


S2 — Links inside body text are indistinguishable from surrounding text [Serious]

WCAG 1.4.1 Use of Color (Level A) · EAA Annex I §III(b)

On 12 paths, inline links have no underline and insufficient contrast against the surrounding paragraph:

Because colour alone carries the link affordance and the colour contrast is below 3:1, users with low vision or colour-vision deficiency cannot identify these as links — including the "complete privacy policy" and "file a vulnerability disclosure report here" links, which are legally significant destinations.

Fix: add a persistent underline to the inline link style preset (HWptGYMjj), or provide a ≥3:1 contrast difference plus a non-colour cue. An underline is a one-property change in the preset and fixes every instance at once.


S3 — Touch targets below the 24×24 px minimum [Serious]

WCAG 2.5.8 Target Size (Minimum), Level AA (WCAG 2.2) · 41 of 58 URLs, 90 instances

The most common offenders are the carousel pagination dots ("Scroll to page 1"…"Scroll to page 6"), measured at 14×24 px and 19×24 px with only ~3 px between neighbours, so the "sufficient spacing" exception does not apply either. Example axe output:

Target has insufficient size (49.1px by 16.8px, should be at least 24px by 24px) | Safe clickable space has a diameter of 18px instead of at least 24px.

Also affected: footer navigation links (49×16.8 px, 59×16.8 px) and the Instagram / support text links in the footer.

Fix: wrap the carousel dots in ≥24×24 px hit areas with visible padding (a pure CSS change that leaves the visual design untouched), and add vertical padding to footer link hit areas. Note WCAG 2.5.8 is an AA criterion in WCAG 2.2; if the target is WCAG 2.1 AA only, this is best-practice — but it is exactly the kind of defect that harms older and motor-impaired users of a health app.


S4 — Focusable elements inside aria-hidden containers [Serious]

WCAG 4.1.2 Name, Role, Value (A) · 26 of 58 URLs, 88 instances

Carousel slides that are visually hidden carry aria-hidden="true" but still contain focusable links, so keyboard focus lands on elements that are invisible to screen readers — the user's focus disappears with no announcement:

<li aria-hidden="true"><div class="framer-dyz763-container" data-framer-name="2">
   ... <a href="...">  <-- focusable, inside aria-hidden

Fix: when marking off-screen carousel slides aria-hidden, also set tabindex="-1" on their focusable descendants (or use the inert attribute, which does this and more). One change in the carousel component.


S5 — Unlabelled interactive controls [Serious]

WCAG 4.1.2 (A), 3.3.2 (A) · two distinct widgets

(a) An unlabelled range slider on /hcp, /nl/hcp, /hcp-tobi-test, /nl/hcp-tobi-test — a visible 392 px slider inside the clinician messaging section ("Make sure your patient has the right answer…") has no <label>, no aria-label, no aria-labelledby, no id and labels.length === 0. Critically, this appears on the healthcare-professional page. A screen-reader user encounters "slider, 50" with no indication of what it controls.

<input type="range" min="0" max="100" class="css-or5vhe">

(b) A search field on /support and /nl/support with only a placeholder ("Search..." / "Zoeken...") and no accessible name.

Fix: add aria-label / a <label> to the slider, and a persistent (visually-hidden if necessary) <label> to the search input. Placeholders are not labels — they vanish on input and are inconsistently announced.


S6 — Links with no accessible name [Serious]

WCAG 2.4.4 / 4.1.2 (A) · 14 of 58 URLs, 22 instances

Two recurring unnamed-link patterns:

(a) A /press link with no text — present on 24 pages including the homepage:

<a class="framer-l8Pg5 framer-todmyg" data-framer-name="Default" href="./press" style="width:100%">

axe: "Element is in tab order and does not have accessible text." A keyboard or screen-reader user hits a stop that announces only "link".

(b) A <span> carrying href and role="link" inside the press-logo carousel (8 paths) — the pattern is a span, not an anchor, so it is announced as a link with no name:

<span as="span" class="framer-194une4" href="https://www.parool.nl/..." role="link" data-nested-link="true">

Fix: add visible text or an aria-label (e.g. aria-label="Read Ditto in the press") to the /press link; replace the span-as-link with a real <a> carrying the publication name as text.


S7 — Images without alt [Serious]

WCAG 1.1.1 Non-text Content (Level A) · 2 paths, 4 instances

The YouTube poster thumbnails on /new-in-ditto/new-in-ditto-calendar-sync-ditto (and its NL twin) have no alt attribute at all:

<img decoding="async" src="https://i.ytimg.com/vi_webp/VtNtt1OIeac/sddefault.webp" style="...">

This is a regression relative to the rest of the site — 180 of 180 images on the homepage carry alt (the large majority correctly empty/decorative), which shows the practice is normally followed.

Fix: add alt="" for decorative poster frames, or a descriptive alt naming the video.


M1 — Heading structure and landmarks [Moderate, high volume]

WCAG 1.3.1 Info and Relationships (A) · affects most pages

Aggravating detail: the cookie banner's <h2>This website uses cookies</h2> is the first heading in the DOM on every page, so screen-reader users navigating by heading hear the consent notice as the page's top-level heading — on /about, which has no h1, it is effectively the page's primary heading.

Fix: one h1 per page matching the page's main title; correct the level sequence in the Framer text styles; wrap page content in <main>; add a visually-hidden-until-focused "Skip to content" link as the first focusable element. These are template-level fixes in the Framer master components and will resolve across all 58 pages at once.


M2 — Auto-playing video with no pause mechanism [Moderate]

WCAG 2.2.2 Pause, Stop, Hide (Level A) · 44 of 116 loads

Nine looping <video> elements render on /download-the-app alone; one plays automatically and continuously. Verified properties: muted: true, autoplay: true, loop: true, controls: false, hasAudio: false, captions: false, and no pause/stop control anywhere on the page.

Because the videos are muted and carry no audio track, WCAG 1.2.2 Captions does not apply and 1.4.2 Audio Control does not apply — correctly reported here rather than over-called. However 2.2.2 applies to any motion that starts automatically and runs longer than five seconds in parallel with other content.

We also confirmed this specifically: prefers-reduced-motion: reduce does not stop the video (1 visible video still playing), even though Ditto's Framer setup does correctly reduce other decorative animation. The reduced-motion handling is therefore partial.

Fix: stop auto-playing decorative video when prefers-reduced-motion: reduce is set (usually a one-line CSS/JS guard), and/or provide a visible pause control. Because the videos are decorative and audio-free, respecting the reduced-motion preference is the proportionate fix.


M3 — Forms: no autocomplete, and error feedback relies entirely on the browser [Moderate–Serious]

WCAG 1.3.5 Identify Input Purpose (AA), 3.3.1 Error Identification (A) · /dach, /nl/dach, /support, /nl/support

Only four pages carry forms; the five "waitlist" pages (/waitlist-ireland, /lista-oczekujacych-polska, /lista-de-espera-espana and their thank-you pages) contain no form elements at all — see journey §6.4.

(a) autocomplete is absent on every field. Verified on all four form pages:

<input type=text  name=Name  required aria-label="" autocomplete=- ph="Anna"            wrapped=YES>
<input type=email name=Email required aria-label="" autocomplete=- ph="kontakt@mail.com" wrapped=YES>
<input type=checkbox name=Consent required ...>

Name and Email are explicitly listed input purposes in WCAG 1.3.5, so this is a Level AA failure. It matters in a health context: without autocomplete, users with cognitive disabilities, memory impairments, or motor impairments cannot use browser autofill on the fields that matter most — and the /dach form is the German-market acquisition funnel.

Fix: add autocomplete="name" and autocomplete="email" to the corresponding fields.

(b) Error feedback is browser-native only. Submitting /support empty produces:

novalidate              : false        (native validation is active — good)
submit event fired      : false        (browser correctly blocks submission)
validationMessage Name  : "Please fill out this field."
aria-invalid count      : 0            <-- not set
live regions on page    : []           <-- none
visible error text      : none inside the form
focus after submit      : moves to Name (correct)

So the user does get a browser validation bubble and focus is moved to the first invalid field — which is reasonable behaviour and why this is not reported as a flat 3.3.1 failure. But there is no persistent error text, no aria-invalid, no aria-describedby association, and no live region. Two consequences: the error state disappears with the transient bubble, and the message is rendered in the browser's language rather than the page's — so an English-configured browser shows "Please fill out this field." on the Dutch /nl/support page, which is a language-of-parts mismatch on a Dutch-language form.

Fix: implement explicit inline error text with aria-invalid="true" and aria-describedby pointing at the message, wrapped in an aria-live="polite" region, using the page's own language. Retain required for native validation as a progressive enhancement.

Credit where due: the honeypot spam fields are handled well — 11 hidden inputs are correctly given tabindex="-1" and aria-hidden="true", so they are neither tabbable nor exposed to assistive technology. This is often botched; here it is right.


6. User-journey analysis

Automated rules find defects; journeys reveal whether people can actually complete tasks. Six end-to-end journeys were walked.

6.1 Journey A — "Dutch patient arrives and switches language" → FAILS (Blocker B1)

Land on dittocare.comopen language selectorchoose Nederlandsread the site.

Every step works visually. The URL becomes /nl/, the title and copy become Dutch, navigation becomes "Over ons / Nieuws / Ondersteuning". But <html lang> stays en. A screen-reader user is now listening to Dutch medical information pronounced by an English synthesizer. Reloading fixes it — which no user would know to do. This is the single most damaging finding, and it hits Ditto's core Dutch market on Ditto's core value proposition: comprehension.

6.2 Journey B — "Keyboard-only user reaches the content" → BLOCKED (Blocker B2)

Load any pagepress Tab.

Focus lands in the cookie banner and cycles: Show details → Allow all → Customize → Deny → Show details → … The user cannot reach navigation, headline, or content. Meanwhile a mouse user at the same moment can scroll and click page content freely (confirmed via elementFromPoint). The journey is completable only after dismissing the banner — but the blocker is invisible to mouse users, so it will never be reported by the people who build the site. Once dismissed, the remainder of the keyboard journey is good: visible focus indicators throughout, and the mobile menu is fully keyboard-operable (Enter, Space, Escape all correct).

6.3 Journey C — "Mobile user opens the menu" → PASSES for keyboard, FAILS for screen readers (Blocker B3)

Load on a 390px viewportTab to the menu controlactivatechoose About.

Enter and Space open the menu; focus order through the menu is logical (Home → Professionals → Privacy → About → News → Support → email → phone → language selector → social → Download App); Escape closes it and returns focus to the trigger. Keyboard-wise this journey is sound.

But the control is an unnamed <div> with no role and no aria-expanded. A screen-reader user hears an unlabelled group, cannot tell what it does, and cannot tell whether it is open. Same task, same outcome for sighted keyboard users, but a dead end for screen-reader users — precisely the kind of gap automated tools under-report.

6.4 Journey D — "Join the waitlist (Ireland / Poland / Spain)" → NO FORM EXISTS

/waitlist-ireland, /lista-oczekujacych-polska, /lista-de-espera-espana and their localized twins are large, fully-built landing pages (657–690 KB) — but they contain zero <input>, zero <form>, zero email capture:

waitlist-ireland:          inputs=0  forms=0
lista-oczekujacych-polska: inputs=0  forms=0
waitlist-ireland-thankyou: inputs=0  forms=0

Meanwhile /waitlist-ireland-thankyou exists and says "Thank you! You are on the waitlist for Ditto in Ireland."

This is a conversion/QA issue with accessibility consequences, not primarily an accessibility defect. Either the waitlist capture lives on an untested third-party surface, or a form was removed and its landing/thank-you pages were left orphaned. It is worth resolving because a third-party embedded form would need its own accessibility assessment, and because the thank-you pages are reachable only by direct URL — meaning users of a broken funnel land on a "thank you" with no accessible path back into the site beyond the shared footer.

The only genuine data-capture journeys are /dach (DACH market) and /support.

6.5 Journey E — "Ask for help via the support form" → COMPLETABLE, with friction

/supportfill Name, Email, Messagesubmit.

Labels are correctly wrapped, fields are required, native validation blocks empty submission, and focus moves to the first invalid field. The journey completes. Friction points: no autocomplete on Name/Email (1.3.5 AA failure); no aria-invalid or live region, so a screen-reader user gets no persistent indication of what failed; the FAQ categories above the form use the broken role="tab" pattern (B4), making it harder to find help before the form; and the unlabelled search field (S5b) sits in the same journey.

6.6 Journey F — "Clinician evaluates Ditto on /hcp" → FAILS for screen-reader users (S5a)

/hcpread evidence and interactive democontact.

The page carries an interactive 392 px slider with no accessible name. For the healthcare-professional audience — the audience most likely to be running an assistive-technology assessment or a procurement accessibility review — an unlabelled interactive control on the primary professional landing page is a visible quality signal, independent of direct user harm. The same page also has heading-order violations and content outside landmarks.

6.7 Journey G — "Zoom / large-text user reads the site" → PASSES

This is the strongest area of the audit. At 320 CSS px there is no horizontal scrolling on any page tested (docW === vw, overflowX: false); at 200% zoom there is likewise no horizontal overflow and no content clipping; and under the WCAG 1.4.12 text-spacing override (line-height 1.5, letter-spacing 0.12em, word-spacing 0.16em, paragraph spacing 2em) content still reflows without loss. This is a meaningful achievement on a highly designed Framer site and should be preserved as changes are made.


7. EAA-specific obligations beyond WCAG

WCAG conformance covers most of Annex I Section III, but the EAA adds obligations that a WCAG audit alone will not surface. These are genuine gaps in Ditto's current position.

7.1 Accessibility information and support services — Annex I §III(b) and §III(d)

The EAA requires, for services, that information about the service's accessibility characteristics and its interoperability with assistive devices be provided through more than one sensory channel, and that support services (help desks, technical support, training) provide information on accessibility in accessible modes of communication.

Ditto's site has /support (email + phone + form) but no accessibility statement, no accessibility conformance documentation, and no published information about assistive-technology compatibility. There is no way for a disabled user to learn, before committing, whether Ditto works with their screen reader, whether transcripts are screen-reader accessible, or how to get help in an accessible format.

Recommendation: publish an accessibility statement covering the site and the app — conformance status against WCAG 2.1 AA, known limitations, the assistive technologies tested, a feedback route, and an enforcement/complaints route. This is the highest-value non-code deliverable, it is expected under the EAA's information requirements, and it is the document a hospital or insurer will request first.

7.2 Feedback and enforcement route — Annex I §III(d), Article 13

The EAA requires Member States to provide a mechanism for users to report non-compliance. Ditto should name its own accessible feedback channel on the accessibility statement. Currently the only route is the generic /support form — which itself has the accessibility defects in M3.

7.3 The Ditto mobile app — Annex I §III(c) — out of scope of this audit

Annex I §III(c) explicitly covers "mobile device-based services, including mobile applications". The Ditto app is the actual product and is squarely in scope. It has not been tested here and must be audited separately: mobile screen-reader support (VoiceOver/TalkBack), text scaling/Dynamic Type, minimum touch targets, colour contrast, and — given the product records and transcribes consultations — captions/transcripts and audio-alternative handling, which under the EAA's functional performance criteria must work for users who are deaf or hard of hearing.

7.4 Functional performance criteria — Annex I Section VII

Where specific technical requirements don't cover a function, Section VII criteria apply directly. Two are worth flagging on the basis of what was found:

7.5 E-commerce specifics — Annex I §IV(g)

If Ditto concludes consumer subscriptions through the website or app, §IV(g) requires identification, security and payment functionality to be perceivable, operable, understandable and robust. The checkout/subscription flow was not reachable from the public website for testing (app-store mediated). It should be assessed directly, including whether payment can be completed without a mouse and whether identification methods are compatible with assistive technology.

7.6 Microenterprise exemption — Article 4(5)

Restated from §3.3 because it is decisive: if Ditto employs fewer than 10 people and has turnover or a balance sheet total ≤ €2m, it is exempt from the EAA's service accessibility requirements. This can only be resolved with Ditto's own headcount and financial data. Even if exempt, the findings here are ordinary usability defects in a patient-facing health product used by people who are unwell, older, or temporarily impaired — and procurement-driven EN 301 549 requirements would reintroduce them.


8. Remediation roadmap

Sequenced by (impact ÷ effort). Items 1–4 are days of work and clear the four blockers.

Phase 1 — Blockers (recommended: within 1 sprint)

#FixWhereEffort
1Set document.documentElement.lang on client-side locale changeRouter/locale handlerHours
2Make the consent banner a real modal (role="dialog" + aria-modal="true") or remove its focus trapCookiebot CMP configHours
3Rebuild the mobile menu control as <button aria-label aria-expanded aria-controls>Framer master componentHours
4Fix /support categories — prefer plain aria-expanded disclosure buttons over the tab patternSupport page component~1 day

Phase 2 — High-volume, template-level (1–2 sprints)

#FixCovers
5Darken 9 colour-token pairs (§S1)125 instances / 27 paths
6Underline the inline link style preset HWptGYMjj (§S2)50 instances / 12 paths
7Enlarge carousel dots and footer link hit areas to ≥24×24 px (§S3)90 instances / 41 paths
8Add tabindex="-1" / inert to focusables inside aria-hidden carousel slides (§S4)88 instances / 26 paths
9Label the /hcp slider and the /support search field (§S5)4 + 2 paths
10Name the unnamed /press link and the span-as-link in the press carousel (§S6)22 instances / 14 paths
11Add alt to the YouTube poster images (§S7)4 instances / 2 paths

Phase 3 — Structure and content (parallel, 1–2 sprints)

#Fix
12One h1 per page; fix heading level sequences; wrap content in <main> (§M1)
13Add a "Skip to content" link as the first focusable element of every page (§M1)
14Stop auto-playing video under prefers-reduced-motion: reduce; add a pause control (§M2)
15Add autocomplete="name" / autocomplete="email"; implement inline errors with aria-invalid + aria-describedby + live region (§M3)
16Resolve the orphaned waitlist pages / missing waitlist form (§6.4)

Phase 4 — EAA programme work

#Item
17Confirm microenterprise status (headcount + turnover) to settle EAA applicability (§3.3)
18Publish an accessibility statement for the website and the app (§7.1)
19Audit the Ditto mobile app against WCAG 2.1 AA / EN 301 549 (§7.3)
20Audit the subscription/checkout flow against Annex I §IV(g) (§7.5)
21Stand up a scripted screen-reader pass (NVDA on Windows, VoiceOver on iOS) to confirm announcement wording and order (§2.3)
22Add accessibility checks to CI — axe-core on pull requests, plus a regression test asserting <html lang> after locale switch, to prevent B1 recurring

Appendix — artefacts and reproducibility

FileContents
axe-raw.jsonFull axe-core results, 116 page loads (violations + incomplete, with element targets)
static-report.jsonSource-HTML analysis of all 58 pages
journeys.jsonBehavioural results — keyboard walks, motion, reduced-motion, reflow/zoom/text-spacing, mobile nav, forms
reference-EAA-AnnexI.txtExtracted EAA Annex I text used for the mapping in §7
axe-audit.mjs, analyze.mjs, journeys.mjs, rollup.mjs, detail.mjsAudit harnesses
run.shRootless Chromium environment wrapper
shots/Screenshots of key states (forms before/after submit, page renders)

External sources consulted for the legal analysis:

Limitations. Automated tools detect roughly 30–40% of WCAG issues; this audit supplements them with scripted behavioural testing and manual DOM inspection, but no human screen-reader session and no testing with disabled users was performed. 3,123 "incomplete" nodes require manual review (chiefly colour-contrast cases over images/gradients, and target-size edge cases). Dynamic states beyond those exercised — full checkout, account creation, app onboarding, logged-in surfaces — were not reachable from the public site. A conformance claim should not be made until Phase 1–3 fixes are verified and Phase 4 item 21 is complete.