Toegankelijke gebruikersinterface voor de PA: WCAG 2.1 AA-implementatie
Hoe toegankelijke gebruikersinterfaces voor de Italiaanse overheid te implementeren: WCAG 2.1 AA, schermlezer, toetsenbordnavigatie, geautomatiseerd testen met MAUVE++ en axe-core, en compliance aan de Italiaanse wetgeving inzake digitale toegankelijkheid (Legge Stanca en AGID).
Digitale toegankelijkheid in de PA: verplichting, geen optie
De toegankelijkheid van de websites en mobiele applicaties van de Italiaanse overheid is dat wel geregeerd door Wet 9 januari 2004, n. 4 (Wet Stanca), bijgewerkt door Wetgevend besluit 106/2018 die de EU-richtlijn 2016/2102 over de toegankelijkheid van websites implementeerde lichamen uit de publieke sector. De technische referentierichtlijnen zijn: WCAG 2.1 (Richtlijnen voor toegankelijkheid van webinhoud) op nalevingsniveau AA.
Vanaf 23 september 2020 moeten alle PA’s een Toegankelijkheidsverklaring op haar website, die jaarlijks wordt bijgewerkt. AgID bewaakt de naleving via het geautomatiseerde systeem MAUVE++ (ontwikkeld met CNR) dat HTML- en CSS-code analyseert op basis van 31 van de 50 criteria succesvol WCAG 2.1 niveau A en AA.
Van de 28 juni 2025 treedt in werking Europese Toegankelijkheidswet (EAA), EU-richtlijn 2019/882, die de toegankelijkheidsverplichting ook uitbreidt naar digitale producten en diensten particulieren: e-commerce, bankwezen, transportdiensten, media. Voor de PA versterkt de EAA de bestaande verplichtingen.
Wat je gaat leren
- De 4 WCAG (POUR) principes en de meest kritische succescriteria voor PA
- Semantische HTML: de onmisbare basis voor elke toegankelijke implementatie
- ARIA (Accessible Rich Internet Applications): wanneer moet u het gebruiken en wanneer moet u het vermijden
- Toetsenbordnavigatie: focusbeheer, links overslaan en zichtbare focusmodi
- Schermlezers: hoe VoiceOver, NVDA en JAWS de DOM interpreteren
- Geautomatiseerd testen: axe-core, MAUVE++, Lighthouse, Pa11y
- Handmatig testen: scenario's met echte schermlezers
- AGID Toegankelijkheidsverklaring: structuur en publicatie
De 4 WCAG-principes: POUR
WCAG 2.1 is gebaseerd op 4 fundamentele principes, bekend onder het acroniem Giet:
| Beginsel | Beschrijving | Belangrijkste criteria (AA) | Veel voorkomende PA-fouten |
|---|---|---|---|
| Waarneembaar | Informatie moet op een waarneembare manier gepresenteerd kunnen worden | Alt-tekst, contrast 4,5:1, ondertiteling, audiobeschrijvingen | Afbeeldingen zonder alts, pdf's niet toegankelijk, video's zonder ondertitels |
| Bedienbaar | Alles moet bruikbaar zijn vanaf het toetsenbord en binnen voldoende tijd | Toetsenbord toegankelijk, links overslaan, geen inhoud die aanvallen veroorzaakt | Alleen muismenu, time-outs te kort, focus niet zichtbaar |
| Begrijpelijk | De inhoud en interface moeten begrijpelijk zijn | Paginataal, formulierlabels, foutpreventie | Formulier zonder label, niet-beschrijvende fouten, ontbrekende taal |
| Robuust | De inhoud moet interpreteerbaar zijn door huidige en toekomstige ondersteunende technologieën | Geldige HTML-parsering, naam/rol/waarde voor alle componenten | Ongeldige HTML, slecht gebruikte ARIA, aangepaste componenten zonder rollen |
Semantische HTML: de basis van alles
De eerste regel van webtoegankelijkheid is het gebruik van correcte semantische HTML. Voordat u ARIA toepast,
zorg ervoor dat de native HTML-elementen al de juiste structuur en rol communiceren naar de
schermlezers. Semantische HTML is inherent toegankelijk; het probleem ontstaat wanneer ze worden gebruikt
generieke elementen (<div>, <span>) voor functionaliteit die HTML
inheemse handelt beter.
<!-- SBAGLIATO: struttura non semantica -->
<div class="header">
<div class="nav">
<div class="nav-item" onclick="navigate('/home')">Home</div>
<div class="nav-item" onclick="navigate('/servizi')">Servizi</div>
</div>
</div>
<div class="main-content">
<div class="article-title">Titolo del Servizio</div>
<div class="content">...</div>
</div>
<!-- CORRETTO: HTML semantico accessibile -->
<header>
<nav aria-label="Navigazione principale">
<ul>
<li><a href="/home">Home</a></li>
<li><a href="/servizi">Servizi</a></li>
</ul>
</nav>
</header>
<main id="main-content"> <!-- id per skip link -->
<article>
<h1>Titolo del Servizio</h1>
<p>...</p>
</article>
</main>
<!-- Elementi landmark per screen reader -->
<!-- header, nav, main, aside, footer sono landmark ARIA nativi -->
<!-- Lo screen reader NVDA li annuncia come: "banner", "navigation", "main", etc. -->
Toegankelijke formulieren: het kritieke punt voor PA-diensten
Onlineformulieren vormen het hart van de interactie tussen burgers en PA: aanvragen van documenten, registraties, declaraties, betalingen. Hier komen ook de meeste toegankelijkheidsfouten voor frequenter en met meer impact. Een ontoegankelijke vorm sluit feitelijk burgers met een handicap uit van toegang tot openbare diensten.
<!-- Form accessibile per servizi PA -->
<!-- Criteri WCAG coinvolti: 1.3.1, 1.3.2, 2.4.6, 3.3.1, 3.3.2, 3.3.4 -->
<form novalidate aria-labelledby="form-title" aria-describedby="form-desc">
<h2 id="form-title">Richiesta Certificato di Residenza</h2>
<p id="form-desc">
Compila tutti i campi obbligatori (contrassegnati con *).
Il certificato verrà inviato all'indirizzo email indicato entro 3 giorni lavorativi.
</p>
<!-- Skip link per saltare al contenuto principale -->
<a href="#form-start" class="skip-link">Vai al form</a>
<fieldset id="form-start">
<legend>Dati Anagrafici</legend>
<!-- Campo con label esplicita e messaggio errore associato -->
<div class="form-field">
<label for="codice-fiscale">
Codice Fiscale
<span aria-hidden="true" class="required-marker">*</span>
<span class="sr-only">(obbligatorio)</span>
</label>
<input
type="text"
id="codice-fiscale"
name="codice-fiscale"
required
aria-required="true"
aria-describedby="cf-hint cf-error"
autocomplete="on"
pattern="[A-Z0-9]{16}"
maxlength="16"
>
<span id="cf-hint" class="field-hint">
16 caratteri alfanumerici (es. RSSMRA85T10A562S)
</span>
<span id="cf-error" class="field-error" role="alert" aria-live="polite">
<!-- Popolato dinamicamente in caso di errore -->
</span>
</div>
<!-- Select accessibile -->
<div class="form-field">
<label for="tipo-documento">
Tipo di documento richiesto
<span aria-hidden="true">*</span>
<span class="sr-only">(obbligatorio)</span>
</label>
<select id="tipo-documento" name="tipo-documento" required aria-required="true">
<option value="">Seleziona...</option>
<option value="residenza">Certificato di Residenza</option>
<option value="stato-famiglia">Certificato di Stato di Famiglia</option>
<option value="nascita">Certificato di Nascita</option>
</select>
</div>
</fieldset>
<!-- Gruppo radio button con fieldset/legend -->
<fieldset>
<legend>Modalità di consegna</legend>
<div class="radio-group">
<input type="radio" id="consegna-email" name="consegna" value="email" checked>
<label for="consegna-email">Via email (PDF)</label>
</div>
<div class="radio-group">
<input type="radio" id="consegna-sportello" name="consegna" value="sportello">
<label for="consegna-sportello">Ritiro allo sportello</label>
</div>
</fieldset>
<button type="submit">Invia Richiesta</button>
</form>
<style>
/* Skip link visibile solo al focus - pattern essenziale per navigazione tastiera */
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px;
background: #000;
color: #fff;
z-index: 9999;
transition: top 0.2s;
}
.skip-link:focus {
top: 0;
}
/* Screen reader only: testo visibile solo allo screen reader */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Focus visibile: WCAG 2.4.7 (AA) e 2.4.11 (AA in WCAG 2.2) */
:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
</style>
LUCHT: wanneer moet u het gebruiken (en wanneer moet u het vermijden)
ARIA (Accessible Rich Internet Applications) breidt HTML uit met attributen die rollen, statussen en eigenschappen communiceren naar ondersteunende technologieën. De eerste regel van ARIA luidt: "Als u een native HTML-element kunt gebruiken met de semantiek en gedrag dat u nodig heeft, is al ingebouwd, in plaats van een element opnieuw te gebruiken en een ARIA-rol toe te voegen, staat of eigendommen om het toegankelijk te maken, doe dat dan".
AIR wordt onmisbaar voor aangepaste widgets die native HTML niet dekt: modals, accordeon, tabbladen, schuifregelaar, combobox met automatische aanvulling. In deze gevallen is ARIA het hulpmiddel correct om semantiek aan de schermlezer over te brengen.
<!-- Esempio: Modal accessibile per PA (conferma azione critica) -->
<!-- Criteri WCAG: 2.4.3 Focus Order, 4.1.2 Name/Role/Value -->
<!-- Trigger -->
<button
type="button"
id="open-modal-btn"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="confirm-modal"
>
Conferma Eliminazione Pratica
</button>
<!-- Modal -->
<div
id="confirm-modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
aria-hidden="true"
tabindex="-1"
>
<div class="modal-inner">
<h2 id="modal-title">Conferma Eliminazione</h2>
<p id="modal-desc">
Sei sicuro di voler eliminare la pratica #PR-2024-001?
Questa azione è irreversibile.
</p>
<div class="modal-actions">
<button type="button" id="modal-cancel">Annulla</button>
<button type="button" id="modal-confirm" class="btn-danger">
Elimina definitivamente
</button>
</div>
</div>
</div>
<!-- JavaScript per focus trap nel modal -->
<script>
class AccessibleModal {
constructor(modalId, triggerId) {
this.modal = document.getElementById(modalId);
this.trigger = document.getElementById(triggerId);
this.focusableSelectors = [
'button:not([disabled])',
'[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(',');
}
open() {
this.modal.removeAttribute('aria-hidden');
this.trigger.setAttribute('aria-expanded', 'true');
this.lastFocus = document.activeElement;
// Focus sul primo elemento focusabile del modal
const firstFocusable = this.modal.querySelectorAll(this.focusableSelectors)[0];
if (firstFocusable) firstFocusable.focus();
// Trap focus nel modal
this.modal.addEventListener('keydown', this._trapFocus.bind(this));
// Chiudi con Escape
document.addEventListener('keydown', this._handleEscape.bind(this));
}
close() {
this.modal.setAttribute('aria-hidden', 'true');
this.trigger.setAttribute('aria-expanded', 'false');
this.modal.removeEventListener('keydown', this._trapFocus.bind(this));
document.removeEventListener('keydown', this._handleEscape.bind(this));
// Restituisce il focus al trigger (WCAG 2.4.3)
this.lastFocus?.focus();
}
_trapFocus(e) {
if (e.key !== 'Tab') return;
const focusable = [...this.modal.querySelectorAll(this.focusableSelectors)];
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
_handleEscape(e) {
if (e.key === 'Escape') this.close();
}
}
</script>
Contrast en typografie: waarneembare criteria
Het WCAG 2.1-criterium 1.4.3 Contrast (minimaal) op AA-niveau vereist een contrastverhouding tenminste 4,5:1 voor platte tekst e 3:1 voor grote tekst (18pt of 14pt vet) en UI-componenten (invoerranden, informatiepictogrammen). Het criterium 1.4.6 Contrast (verbeterd) op AAA-niveau brengt het deze drempels op 7:1 en 4,5:1.
/* CSS: sistema di colori accessibile per siti PA italiani */
/* Ispirato al design system di Designers Italia (UI Kit Italia) */
:root {
/* Palette primaria - tutti i contrasti calcolati e verificati */
/* Bianco su blu scuro: 8.59:1 (supera AAA) */
--color-primary: #0066cc;
--color-primary-dark: #004d99;
--color-on-primary: #ffffff;
/* Testo scuro su bianco: 14.47:1 (supera AAA) */
--color-text-primary: #17324d;
--color-text-secondary: #5b6f82; /* 4.58:1 su bianco - passa AA */
--color-background: #ffffff;
/* Errori: rosso scuro con contrasto 5.12:1 su bianco */
--color-error: #c32f00;
--color-error-bg: #fef0ec; /* sfondo chiaro per messaggi errore */
/* Attenzione: sfondo giallo scuro, testo scuro */
--color-warning: #6b4700; /* 7.03:1 su sfondo warning-bg */
--color-warning-bg: #fff6d6;
/* Successo */
--color-success: #1a6600; /* 6.56:1 su sfondo success-bg */
--color-success-bg: #e5f7e0;
/* Dimensioni minime testo */
--font-size-base: 1rem; /* 16px - minimo per body */
--font-size-small: 0.875rem; /* 14px - mai sotto questa soglia */
--line-height-base: 1.6; /* WCAG raccomanda >= 1.5 */
--letter-spacing-base: 0.02em; /* Migliora leggibilità */
}
/* Focus visibile: non togliere mai l'outline! */
/* WCAG 2.4.7 (AA): ogni elemento deve avere uno stato di focus visibile */
:focus-visible {
outline: 3px solid var(--color-primary);
outline-offset: 2px;
/* NON: outline: none !important; --- questo viola WCAG */
}
/* Testo leggibile: non solo contrasto, anche dimensioni */
body {
font-size: var(--font-size-base);
line-height: var(--line-height-base);
color: var(--color-text-primary);
background: var(--color-background);
}
/* Rispetta le preferenze di movimento ridotto */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Rispetta la preferenza per testo grande del sistema operativo */
@media (prefers-contrast: high) {
:root {
--color-primary: #003d7a;
--color-text-secondary: #2c3e50;
}
}
Geautomatiseerd testen: axe-core, MAUVE++ en Lighthouse
Geautomatiseerde toegankelijkheidstests kunnen ongeveer de 30-40% van de WCAG-fouten, volgens schattingen van Deque Systems. De resterende 60-70% vereist handmatige tests met echte schermlezers. Echter, automatisering is essentieel om regressies in de CI/CD-cyclus op te vangen.
# Testing accessibilità con axe-core in Python (Playwright)
# Integrazione in CI/CD per catturare regressioni
import asyncio
from playwright.async_api import async_playwright, Page
from axe_playwright_python import Axe
import json
async def run_accessibility_audit(url: str, output_file: str = "a11y-report.json"):
"""
Esegue audit WCAG 2.1 AA con axe-core via Playwright.
Restituisce violazioni categorizzate per severità.
"""
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
# Simula utente con screen reader (viewport ridotto + riduzione movimento)
await page.emulate_media(reduced_motion="reduce")
await page.goto(url, wait_until="networkidle")
axe = Axe()
results = await axe.run(
page,
options={
"runOnly": {
"type": "tag",
"values": ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]
}
}
)
# Categorizza per severità
violations_by_impact = {
"critical": [],
"serious": [],
"moderate": [],
"minor": []
}
for violation in results.violations:
impact = violation.impact or "minor"
violations_by_impact[impact].append({
"id": violation.id,
"description": violation.description,
"help_url": violation.help_url,
"nodes": len(violation.nodes),
"elements": [node.html for node in violation.nodes[:3]] # Max 3 esempi
})
report = {
"url": url,
"timestamp": asyncio.get_event_loop().time(),
"violations_total": len(results.violations),
"passes": len(results.passes),
"incomplete": len(results.incomplete),
"violations_by_impact": violations_by_impact,
"wcag_coverage": "WCAG 2.1 AA"
}
with open(output_file, "w") as f:
json.dump(report, f, indent=2, ensure_ascii=False)
# Fallisce la CI se ci sono violazioni critical/serious
critical_count = len(violations_by_impact["critical"])
serious_count = len(violations_by_impact["serious"])
if critical_count + serious_count > 0:
print(f"FAIL: {critical_count} critical, {serious_count} serious violations")
return False
print(f"PASS: 0 critical/serious violations, {len(results.violations)} total")
await browser.close()
return True
# Utilizzo in pipeline CI/CD
async def ci_accessibility_check():
pages_to_check = [
"https://servizi.comune.esempio.it/",
"https://servizi.comune.esempio.it/richiesta-certificato",
"https://servizi.comune.esempio.it/login",
]
all_pass = True
for url in pages_to_check:
result = await run_accessibility_audit(url, f"a11y-{url.split('/')[-1] or 'home'}.json")
all_pass = all_pass and result
return 0 if all_pass else 1
asyncio.run(ci_accessibility_check())
Handmatig testen met schermlezer
Handmatig testen met schermlezers is onvervangbaar. De belangrijkste schermlezers die u kunt testen zijn:
- NVDA (Niet-visuele desktoptoegang) + Chrome/Firefox: het meest gebruikt op Windows, gratis, open source. Standaardcombinatie in AGID-tests.
- JAWS (Taaktoegang met spraak) + Chrome: de populairste professionele schermlezer, tegen betaling. Gebruikt door veel professionele gebruikers.
- VoiceOver + Safari: op macOS en iOS. Essentiële test voor Apple-apparaten.
- TalkBack + Chrome: native Android-schermlezer. Fundamenteel voor mobiele diensten.
Handmatige testchecklist met schermlezer
- Navigeer alleen met Tab over de hele pagina: elk interactief element moet bereikbaar en bruikbaar zijn
- Controleer of de paginatitel wordt aangekondigd bij het landen (WCAG 2.4.2)
- Test de formulieren: elk label moet vóór het veld worden gelezen, fouten moeten worden aangekondigd
- Testmodaliteiten: de focus moet worden verplaatst naar het modale bij het openen en terug naar de trigger bij het sluiten
- Controleer de kopstructuur: deze moet logisch zijn en mag geen niveaus overslaan (h1 > h2 > h3)
- Testtabellen: rij- en kolomkoppen moeten aan cellen zijn gekoppeld (bereik, id/headers)
- Controleer de links: "Klik hier" is niet toegankelijk; de linktekst moet de bestemming beschrijven
- Test live-meldingen: aria-live = "beleefd" voor niet-dringende updates, "assertief" voor kritieke fouten
AGID Toegankelijkheidsverklaring
Elke PA moet op haar website a Toegankelijkheidsverklaring conform aan de structuur gedefinieerd door AGID (Europees model). De verklaring moet het volgende bevatten: nalevingsstatus (conform / gedeeltelijk conform / niet-conform), inhoud niet toegankelijk met de reden, contacten voor rapporten, link naar het AGID-rapportagemechanisme en datum van de laatste update.
Bereikbaarheidsdoelstellingen moeten jaarlijks worden gepubliceerd door 31 maart, zoals beoogd in het Driejaren ICT Plan 2024-2026. Niet publiceren of de verklaring wordt niet bijgewerkt wordt bestraft door AGID.
Conclusies en volgende stappen
WCAG 2.1 AA-toegankelijkheid is niet alleen een wettelijke vereiste: het is een indicator van kwaliteit van de code en zorg voor gebruikers. Een toegankelijke interface werkt voor iedereen beter: navigatie Het toetsenbord helpt ervaren gebruikers, hoge contrasten verbeteren de leesbaarheid in heldere omstandigheden moeilijke, alternatieve teksten verbeteren SEO.
Met de inwerkingtreding van de Europese Toegankelijkheidswet in juni 2025 wordt toegankelijkheid ook verplicht voor particuliere bedrijven die digitale diensten aanbieden, waarbij de vaardigheden beschreven worden in dit artikel nog waardevoller op de markt.
Gerelateerde artikelen in deze serie
- GovTech #03: Open Data API Design - publiceer en consumeer openbare gegevens
- GovTech #04: GDPR-by-Design - architecturale patronen voor openbare diensten
- GovTech #06: Overheids-API-integratie - SPID, CIE en pagoPA







