UI Accessibili per la PA: Implementazione WCAG 2.1 AA
Come implementare interfacce utente accessibili per la Pubblica Amministrazione italiana: WCAG 2.1 AA, screen reader, keyboard navigation, testing automatizzato con MAUVE++ e axe-core, e conformità alla normativa italiana sull'accessibilità digitale (Legge Stanca e AGID).
L'Accessibilità Digitale nella PA: Obbligo, Non Opzione
L'accessibilità dei siti web e delle applicazioni mobile della Pubblica Amministrazione italiana è disciplinata dalla Legge 9 gennaio 2004, n. 4 (Legge Stanca), aggiornata dal D.Lgs. 106/2018 che ha recepito la Direttiva UE 2016/2102 sull'accessibilità dei siti web degli organismi del settore pubblico. Le linee guida tecniche di riferimento sono le WCAG 2.1 (Web Content Accessibility Guidelines) al livello di conformità AA.
Dal 23 settembre 2020 tutte le PA devono pubblicare una Dichiarazione di Accessibilità sul proprio sito, aggiornata annualmente. AGID monitora la conformità tramite il sistema automatizzato MAUVE++ (sviluppato con CNR) che analizza il codice HTML e CSS rispetto a 31 dei 50 criteri di successo WCAG 2.1 di livello A e AA.
Dal 28 giugno 2025 entra in vigore il European Accessibility Act (EAA), Direttiva UE 2019/882, che estende l'obbligo di accessibilità anche ai prodotti e servizi digitali privati: e-commerce, banking, servizi di trasporto, media. Per la PA, l'EAA rafforza gli obblighi esistenti.
Cosa Imparerai
- I 4 principi WCAG (POUR) e i criteri di successo più critici per la PA
- Semantic HTML: la base indispensabile per qualsiasi implementazione accessibile
- ARIA (Accessible Rich Internet Applications): quando usarlo e quando evitarlo
- Keyboard navigation: focus management, skip links e modalità focus visibili
- Screen reader: come VoiceOver, NVDA e JAWS interpretano il DOM
- Testing automatizzato: axe-core, MAUVE++, Lighthouse, Pa11y
- Testing manuale: scenari con screen reader reali
- Dichiarazione di Accessibilità AGID: struttura e pubblicazione
I 4 Principi WCAG: POUR
Le WCAG 2.1 si basano su 4 principi fondamentali, noti con l'acronimo POUR:
| Principio | Descrizione | Criteri Chiave (AA) | Errori Comuni PA |
|---|---|---|---|
| Perceivable | Le informazioni devono essere presentabili in modi percepibili | Alt text, contrasto 4.5:1, captions, audio descriptions | Immagini senza alt, PDF non accessibili, video senza sottotitoli |
| Operable | Tutto deve essere utilizzabile da tastiera e in tempo sufficiente | Keyboard accessible, skip links, no seizure-inducing content | Menu solo mouse, timeout troppo brevi, focus non visibile |
| Understandable | Il contenuto e l'interfaccia devono essere comprensibili | Lingua della pagina, etichette form, prevenzione errori | Form senza label, errori non descrittivi, lingua mancante |
| Robust | Il contenuto deve essere interpretabile da tecnologie assistive attuali e future | Parsing HTML valido, name/role/value per tutti i componenti | HTML invalido, ARIA usato male, componenti custom senza ruoli |
Semantic HTML: la Base di Tutto
La prima regola dell'accessibilità web è usare l'HTML semantico corretto. Prima di applicare ARIA,
assicurati che gli elementi HTML nativi stiano già comunicando la struttura e il ruolo corretto agli
screen reader. L'HTML semantico è intrinsecamente accessibile; il problema sorge quando si usano
elementi generici (<div>, <span>) per funzionalità che HTML
nativo gestisce meglio.
<!-- 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. -->
Forms Accessibili: il Punto Critico per i Servizi PA
I moduli online sono il cuore dell'interazione tra cittadini e PA: richiesta di documenti, iscrizioni, dichiarazioni, pagamenti. Sono anche il punto in cui gli errori di accessibilità sono più frequenti e più impattanti. Un form inaccessibile esclude di fatto i cittadini con disabilità dall'accesso ai servizi pubblici.
<!-- 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>
ARIA: Quando Usarlo (e Quando Evitarlo)
ARIA (Accessible Rich Internet Applications) estende HTML con attributi che comunicano ruoli, stati e proprietà alle tecnologie assistive. La prima regola di ARIA recita: "If you can use a native HTML element with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so".
ARIA diventa indispensabile per i widget personalizzati che HTML nativo non copre: modali, accordion, tabs, slider, combobox con autocompletamento. In questi casi, ARIA è lo strumento corretto per comunicare la semantica allo screen reader.
<!-- 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>
Contrasto e Tipografia: Criteri Percepibili
Il criterio WCAG 2.1 1.4.3 Contrast (Minimum) a livello AA richiede un rapporto di contrasto di almeno 4.5:1 per il testo normale e 3:1 per testo grande (18pt o 14pt bold) e componenti UI (bordi di input, icone informative). Il criterio 1.4.6 Contrast (Enhanced) a livello AAA porta queste soglie a 7:1 e 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;
}
}
Testing Automatizzato: axe-core, MAUVE++ e Lighthouse
Il testing automatizzato dell'accessibilità può rilevare circa il 30-40% degli errori WCAG, secondo le stime di Deque Systems. Il restante 60-70% richiede testing manuale con screen reader reali. Tuttavia, l'automazione è indispensabile per catturare regressioni nel ciclo CI/CD.
# 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())
Testing Manuale con Screen Reader
Il testing manuale con screen reader è insostituibile. I principali screen reader da testare sono:
- NVDA (NonVisual Desktop Access) + Chrome/Firefox: il più usato su Windows, gratuito, open source. Combinazione di default nei test AGID.
- JAWS (Job Access With Speech) + Chrome: lo screen reader professionale più diffuso, a pagamento. Usato da molti utenti professionali.
- VoiceOver + Safari: su macOS e iOS. Test essenziale per i dispositivi Apple.
- TalkBack + Chrome: screen reader nativo Android. Fondamentale per servizi mobile.
Checklist Testing Manuale con Screen Reader
- Naviga l'intera pagina solo con Tab: ogni elemento interattivo deve essere raggiungibile e usabile
- Verifica che il titolo della pagina sia annunciato all'atterraggio (WCAG 2.4.2)
- Testa i form: ogni label deve essere letta prima del campo, gli errori devono essere annunciati
- Testa i modal: il focus deve spostarsi nel modal all'apertura e tornare al trigger alla chiusura
- Verifica la struttura heading: deve essere logica e non saltare livelli (h1 > h2 > h3)
- Testa le tabelle: header di riga e colonna devono essere associati alle celle (scope, id/headers)
- Verifica i link: "Clicca qui" è inaccessibile; il testo del link deve descrivere la destinazione
- Testa le notifiche live: aria-live="polite" per aggiornamenti non urgenti, "assertive" per errori critici
Dichiarazione di Accessibilità AGID
Ogni PA deve pubblicare sul proprio sito web una Dichiarazione di Accessibilità conforme alla struttura definita da AGID (modello europeo). La dichiarazione deve includere: stato di conformità (conforme / parzialmente conforme / non conforme), contenuti non accessibili con la motivazione, contatti per segnalazioni, link al meccanismo di segnalazione AGID, e data dell'ultimo aggiornamento.
Gli obiettivi di accessibilità devono essere pubblicati ogni anno entro il 31 marzo, come previsto dal Piano Triennale ICT 2024-2026. La mancata pubblicazione o la dichiarazione non aggiornata è sanzionabile da AGID.
Conclusioni e Prossimi Passi
L'accessibilità WCAG 2.1 AA non è solo un obbligo normativo: è un indicatore di qualità del codice e della cura verso gli utenti. Un'interfaccia accessibile funziona meglio per tutti: la navigazione da tastiera aiuta i power user, i contrasti elevati migliorano la leggibilità in condizioni di luce difficile, i testi alternativi migliorano la SEO.
Con l'entrata in vigore dell'European Accessibility Act nel giugno 2025, l'accessibilità diventa obbligatoria anche per le aziende private che offrono servizi digitali, rendendo le competenze descritte in questo articolo ancora più preziose sul mercato.
Articoli Correlati in Questa Serie
- GovTech #03: Open Data API Design - pubblicare e consumare dati pubblici
- GovTech #04: GDPR-by-Design - pattern architetturali per servizi pubblici
- GovTech #06: Integrazione API Governative - SPID, CIE e pagoPA

Commenti
Caricamento commenti...
Accedi per commentare