Installs into .claude/skills of the current project.
Are you the author of Accessibility Arttuan?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/thomasmoreai-accessibility-arttuan)
---
name: accessibility-arttuan
title: Accessibility
description: Turn the European Accessibility Act into numbered, testable acceptance criteria against EN 301 549 / WCAG, plus the accessibility statement
author: ArttuAn
author_url: https://github.com/ArttuAn/eu-compliance-skills/tree/main/skills/accessibility
license: MIT
version: 0.1.0
execution_mode: open
jurisdiction: eu
practice: regulatory
language: en
---
# Accessibility
The European Accessibility Act is the regime most likely to apply to an ordinary
product and least likely to be scoped. It is not a design preference: for
covered products and services sold in the EU it is market-access law, enforced
by national authorities, with withdrawal from the market as the remedy.
The good news is that it is the only regime in this repo that is **fully
testable**. Every obligation maps to a numbered clause with a pass/fail check,
much of it automatable. So this skill's job is to convert "make it accessible"
into acceptance criteria a developer can close and CI can enforce.
> **Not legal advice.** Scope, the microenterprise exemption and any
> disproportionate-burden claim are legal judgements. This skill produces the
> testable criteria and the gaps.
```
in scope? ──no──► record the exclusion + expires_if
│ yes
▼
EN 301 549 clauses that apply to this surface
│
▼
numbered acceptance criteria ──► automated (~30%) + manual (~70%)
│
▼
CI gate · accessibility statement · disproportionate-burden file if claimed
```
## Use this when
- The product is e-commerce, consumer banking, e-books, transport, telephony, or
audiovisual media access — sold to consumers in the EU.
- You are a public sector body in the EU (Web Accessibility Directive
2016/2102), where the duty is older and the accessibility statement is
mandatory and formatted.
- Enterprise procurement is asking for a VPAT or an EN 301 549 conformance
statement, which is increasingly a precondition for selling to anyone large.
**Do not use this when** you are a service microenterprise under both limbs —
fewer than 10 staff **and** turnover or balance-sheet total at or below €2M —
and the product is a service rather than a product. Record the exclusion with
`expires_if: headcount >= 10 or turnover_eur > 2000000`, because it is the
exclusion most likely to expire without anyone noticing.
Build it anyway if you can. Accessibility work done during the build costs a
fraction of accessibility work done under a deadline, and the microenterprise
exemption does not survive growth.
## Ask first
Skip "is it accessible" — nobody can answer it. Ask what can be tested.
1. **"What is the primary task a user comes here to do? Name the top three."**
Accessibility is assessed on journeys, not pages. This scopes the work to
something finishable.
2. **"Can each of those be completed with a keyboard only — no mouse — at 200%
browser zoom, with a screen reader?"**
The honest triage. If the answer is "I don't know", that is the first task.
3. **"Which surfaces are in scope? Web, native mobile, email, PDF documents,
video, a kiosk or terminal?"**
Each has different EN 301 549 clauses. PDFs and video are the two that get
forgotten and are the most expensive to retrofit.
4. **"Headcount, turnover, balance sheet total?"**
The exemption needs both limbs.
5. **"Is there any existing audit, VPAT, or accessibility statement?"**
If yes, its date matters more than its contents. An audit older than the last
redesign is not evidence of anything.
## What the law requires
**Directive (EU) 2019/882 (EAA)** — applies to covered products and services
placed on the EU market. Applied from 28 June 2025; transposed nationally, so
the binding text is the national law. *Verify the national transposition for
each Member State you sell into.*
**Directive (EU) 2016/2102** — public sector bodies' websites and mobile apps,
with a mandatory, formatted accessibility statement and a feedback mechanism.
**EN 301 549** is the harmonised standard. Conformity with a harmonised standard
gives a **presumption of conformity** — a legal effect, and the cheapest route
to compliance. Version 3.2.1 references WCAG 2.1 Level AA; later versions align
with WCAG 2.2. *Check which version is cited in the current OJ list of
harmonised standards for the EAA before declaring conformity against it.*
Target **WCAG 2.2 AA** in practice. It is a superset, the extra criteria are
cheap (focus appearance, dragging alternatives, consistent help, accessible
authentication), and it future-proofs against the standard moving under you.
### The clause groups that matter
| Group | EN 301 549 | What it covers |
| --- | --- | --- |
| Web | § 9 | WCAG A + AA applied to web content |
| Non-web documents | § 10 | PDFs, Office documents, e-books |
| Software | § 11 | Native and desktop applications |
| Support services | § 12 | Help desks, documentation — must itself be accessible |
| ICT with two-way comms | § 6 | Real-time text, video quality |
| Hardware / terminals | § 8 | Kiosks, ATMs, ticket machines |
**§ 12 catches people out.** Your support channel and your documentation are in
scope. A perfectly accessible product whose only help route is an inaccessible
chat widget fails.
### Disproportionate burden
The derogation exists and it is narrow. It must be **assessed, documented,
quantified** — the cost of compliance against the organisation's size and
resources, and the benefit to people with disabilities — and **reassessed
periodically**. It cannot be claimed because the work is inconvenient, and a
claim resting on the cost of a redesign you wanted anyway will not hold.
An undocumented claim of disproportionate burden is not a derogation; it is a
written admission of non-conformity. Treat any claim as a `condition` with a
required document and a review date.
## What to produce
**1. Numbered acceptance criteria**, one per applicable clause, in a form a
developer can close:
```yaml
- id: AC-014
clause: "EN 301 549 § 9.1.4.3 (WCAG 1.4.3 Contrast Minimum)"
surface: web
journey: checkout
criterion: >
All text and images of text have a contrast ratio of at least 4.5:1
(3:1 for text at 18pt / 14pt bold or larger) against their background.
test: automated
tool: "axe-core rule color-contrast"
status: fail
evidence: "reports/axe/checkout-2026-09-15.json"
- id: AC-021
clause: "EN 301 549 § 9.2.4.3 (WCAG 2.4.3 Focus Order)"
surface: web
journey: checkout
criterion: >
Tabbing through the checkout reaches every interactive element in an order
that preserves meaning, and focus never enters a hidden region.
test: manual
method: "keyboard-only walkthrough, documented in reports/manual/checkout.md"
status: pass
```
**Automated tooling catches roughly a third of WCAG failures.** That is worth
having and it is not a conformance claim. axe-core, Lighthouse and pa11y find
missing alt text, contrast, form labels and landmark issues; they cannot tell
you whether the alt text is *right*, whether focus order preserves meaning, or
whether an error message is understandable. Mark each criterion `automated` or
`manual` honestly, and never report an automated pass as conformance.
**2. A CI gate**, on the journeys only:
```yaml
- name: accessibility
run: |
npx playwright test tests/a11y --reporter=json
npx axe-ci --exit-one-on-violation \
--include "$(cat compliance/a11y/journeys.txt)"
```
Gate the primary journeys, not every page. A gate that fails on a marketing page
typo gets disabled, and then the checkout regresses unnoticed.
**3. The accessibility statement**, published and reachable:
- Which standard and version you conform to, and the conformance level.
- **What is not accessible**, specifically, with the reason and the date it will
be fixed. This section is what makes the statement credible; a statement
claiming full conformity with no exceptions is read as a statement nobody
tested.
- Any disproportionate-burden claim, with the assessment referenced.
- A feedback mechanism, and what happens to feedback.
- The date of the last assessment and the method used.
For public sector bodies the statement has a **prescribed model format** in
Commission Implementing Decision (EU) 2018/1523 — use it rather than writing
your own.
**4. Manual test evidence.** A dated walkthrough per journey: keyboard only,
screen reader (NVDA or VoiceOver named, with version), 200% zoom, 320px
viewport. Store it with the report. A manual criterion marked `pass` with no
evidence file is a finding.
**Every assessment carries a certainty.** Each finding, each applicable or
ruled-out regime, and the headline of the brief carry `certainty` (0–100) — the
honest percent of the assessment that rests on verified sources and confirmed
facts rather than inference, open unknowns and interpretation. Wherever you
state an applicability, a classification, a citation-backed obligation or a
finding, show the percentage **and the single largest reason it is not higher**:
"ruled out NIS2 — certainty 66%: the national transposition was not read".
`references/certainty.md` holds the anchors; the rules that matter here: an
answer graded `UNKNOWN` caps anything resting on it at 70, an unread Directive
transposition caps at 60, a classification pending counsel caps at 50, and a
`100` requires every deciding fact SPECIFIC, every citation verified on EUR-Lex,
every transposition read and every regime screened. Certainty is visibility, not
a free pass — VAGUE stays rejected, an open blocker stays blocked.
## Failure modes
- **Automated pass reported as conformance.** axe returns zero violations, the
team declares WCAG AA. Roughly two-thirds of the criteria were never tested.
- **Scoping to pages instead of journeys.** An accessibility programme that
never finishes because the page count grows faster than the fixes. Pick the
top three tasks and make those complete.
- **Forgetting documents, video and support.** PDFs (§ 10), captions and audio
description (§ 7), and the help channel (§ 12). All in scope, all commonly
omitted, all expensive to retrofit.
- **The microenterprise exemption assumed and never re-checked.** Both limbs
required, and it expires with the tenth hire. `expires_if` in the brief.
- **Disproportionate burden claimed without an assessment.** Not a derogation;
an admission.
- **An accessibility statement with no known issues.** Reads as untested,
because it is. List the gaps with dates.
- **A stale audit.** An assessment older than the last redesign is evidence
about software that no longer exists. Date everything and re-test on redesign.
- **Overlays sold as compliance.** Third-party accessibility overlay widgets do
not make a site conformant and are the subject of significant litigation and
negative guidance. If one is in the codebase, that is a finding.
## Verify
Follow `references/verification.md`. For this skill specifically:
1. **Tier 0** — install, `--help`, generate criteria for a fixture and render
the statement.
2. **Tier 1** — `pytest -q`, offline:
```python
def test_every_criterion_cites_a_numbered_clause(criteria):
for ac in criteria:
assert re.match(r"EN 301 549 § \d+\.\d+", ac.clause)
assert "WCAG" in ac.clause
def test_manual_and_automated_are_labelled_honestly(criteria):
automated = [ac for ac in criteria if ac.test == "automated"]
assert len(automated) / len(criteria) < 0.5 # tooling covers ~1/3, not most
def test_automated_pass_is_not_reported_as_conformance(report):
text = render(report).lower()
assert "conforms to wcag" not in text or report.manual_complete
def test_microenterprise_exemption_needs_both_limbs():
assert exempt_service(9, 1_000_000) is True
assert exempt_service(10, 1_000_000) is False
assert exempt_service(9, 3_000_000) is False
def test_exemption_records_an_expiry(brief_micro):
exclusion = assess(brief_micro).ruled_out[0]
assert "headcount >= 10" in exclusion["expires_if"]
def test_support_and_documents_are_in_scope(criteria_for_web_with_pdfs):
clauses = {ac.clause for ac in criteria_for_web_with_pdfs}
assert any("§ 10" in c for c in clauses) # non-web documents
assert any("§ 12" in c for c in clauses) # support services
def test_disproportionate_burden_requires_a_document(brief_claiming_burden):
finding = assess(brief_claiming_burden).finding_for("disproportionate burden")
assert finding.severity == "condition"
assert "quantified assessment" in finding.needs
def test_statement_lists_known_gaps(statement, criteria_with_failures):
assert "not accessible" in statement.lower()
for failing in [ac for ac in criteria_with_failures if ac.status == "fail"]:
assert failing.clause in statement
def test_manual_pass_without_evidence_is_a_finding(criteria):
ac = criteria.by_id("AC-021")
ac.status, ac.evidence = "pass", None
assert validate(ac)[0].severity == "condition"
def test_ci_gate_covers_journeys_not_every_page(config):
assert config.include == read_journeys()
assert "*" not in config.include
def test_overlay_dependency_is_a_finding(repo_with_overlay):
assert any("overlay" in f.statement.lower() for f in scan(repo_with_overlay))
```
3. **Tier 2** — report: criteria total, split automated/manual, pass rate on
each, and journeys fully covered. Lead with the **manual** pass rate, because
the automated one flatters.
4. **Tier 3** — a person who uses a screen reader daily completes the primary
journey. No tool and no checklist substitutes for this, and it finds things
the standard does not describe. Budget for it once before launch.
Then tell the user which EN 301 549 version you assessed against, that you could
not verify which version is currently cited in the OJ, and exactly which
criteria were never tested rather than implying full coverage.