Interfață de utilizare accesibilă pentru PA: implementare WCAG 2.1 AA
Cum se implementează interfețe de utilizator accesibile pentru administrația publică italiană: WCAG 2.1 AA, cititor de ecran, navigare cu tastatură, testare automată cu MAUVE++ și axe-core și conformitate la legislația italiană privind accesibilitatea digitală (Legge Stanca și AGID).
Accesibilitatea digitală în PA: obligație, nu opțiune
Accesibilitatea site-urilor web și a aplicațiilor mobile ale Administrației Publice Italiene este guvernat de Legea 9 ianuarie 2004, nr. 4 (Legea Stanca), actualizată de Decret legislativ 106/2018 care a implementat Directiva UE 2016/2102 privind accesibilitatea site-urilor web organismele din sectorul public. Ghidurile tehnice de referință sunt: WCAG 2.1 (Orientări privind accesibilitatea conținutului web) la nivel de conformitate AA.
Începând cu 23 septembrie 2020, toate AP-urile trebuie să publice a Declarație de accesibilitate pe site-ul său, actualizat anual. AgID monitorizează conformitatea prin intermediul sistemului automatizat MOV++ (dezvoltat cu CNR) care analizează codul HTML și CSS în funcție de 31 din 50 de criterii de succes WCAG 2.1 nivel A și AA.
De la 28 iunie 2025 intră în vigoare la data Actul european de accesibilitate (EAA), Directiva UE 2019/882, care extinde obligația de accesibilitate și la produsele și serviciile digitale persoane fizice: comerț electronic, servicii bancare, servicii de transport, media. Pentru AP, AEA consolidează obligațiile existente.
Ce vei învăța
- Cele 4 principii WCAG (POUR) și cele mai critice criterii de succes pentru PA
- HTML semantic: baza indispensabilă pentru orice implementare accesibilă
- ARIA (Accessible Rich Internet Applications): când să îl utilizați și când să îl evitați
- Navigare prin tastatură: gestionarea focalizării, săriți link-uri și moduri de focalizare vizibile
- Cititoare de ecran: Cum interpretează VoiceOver, NVDA și JAWS DOM
- Testare automată: axe-core, MAUVE++, Lighthouse, Pa11y
- Testare manuală: scenarii cu cititoare de ecran reale
- Declarația de accesibilitate AGID: structură și publicare
Cele 4 principii WCAG: POUR
WCAG 2.1 se bazează pe 4 principii fundamentale, cunoscute prin acronim POUR:
| Principiu | Descriere | Criterii cheie (AA) | Erori PA comune |
|---|---|---|---|
| Perceptibil | Informațiile trebuie să fie prezentabile în moduri perceptibile | Alt text, contrast 4.5:1, subtitrări, descrieri audio | Imagini fără alte, PDF-uri inaccesibile, videoclipuri fără subtitrări |
| Operabil | Totul trebuie să fie utilizabil de la tastatură și în timp suficient | Tastatură accesibilă, săriți peste linkuri, fără conținut care provoacă convulsii | Meniu doar pentru mouse, timeout-uri prea scurte, focalizarea nu este vizibilă |
| De înțeles | Conținutul și interfața trebuie să fie de înțeles | Limba paginii, etichete de formulare, prevenirea erorilor | Formular fără etichetă, erori nedescriptive, limbaj lipsă |
| Robust | Conținutul trebuie să fie interpretabil de tehnologiile de asistență actuale și viitoare | Analiză HTML validă, nume/rol/valoare pentru toate componentele | HTML nevalid, ARIA prost folosit, componente personalizate fără roluri |
HTML semantic: baza tuturor
Prima regulă a accesibilității web este utilizarea HTML semantic corect. Înainte de a aplica ARIA,
asigurați-vă că elementele HTML native comunică deja structura și rolul corecte
cititoare de ecran. HTML semantic este în mod inerent accesibil; problema apare atunci când sunt folosite
elemente generice (<div>, <span>) pentru funcționalitatea HTML
nativ se descurcă mai bine.
<!-- 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. -->
Formulare accesibile: punctul critic pentru serviciile PA
Formularele online sunt inima interacțiunii dintre cetățeni și AP: cerere de documente, înscrieri, declarații, plăți. Ele sunt, de asemenea, acolo unde erorile de accesibilitate sunt cele mai multe frecvente și mai de impact. O formă inaccesibilă exclude efectiv cetățenii cu dizabilități din accesul la serviciile publice.
<!-- 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>
AIR: Când să îl folosiți (și când să îl evitați)
ARIA (Accessible Rich Internet Applications) extinde HTML cu atribute care comunică roluri, stări și proprietăți la tehnologiile de asistență. Prima regulă a ARIA spune: „Dacă puteți utiliza un element HTML nativ cu semantica și comportamentul de care aveți nevoie deja încorporate, în loc să reutilizați un element și să adăugați un rol ARIA, stat sau proprietate pentru a o face accesibilă, apoi faceți acest lucru".
AERUL devine indispensabil pentru widget-uri personalizate acel HTML nativ nu acoperă: modale, acordeon, tab-uri, slider, combobox cu completare automată. În aceste cazuri, ARIA este instrumentul corect pentru a comunica semantica cititorului de ecran.
<!-- 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 și tipografie: criterii perceptibile
Criteriul WCAG 2.1 1.4.3 Contrast (minimum) la nivelul AA necesită un raport de contrast cel putin 4,5:1 pentru text simplu e 3:1 pentru text mare (18 pt sau 14 pt aldine) și componentele UI (chenaruri de intrare, pictograme de informații). Criteriul 1.4.6 Contrast (Îmbunătățit) la nivelul AAA aduce aceste praguri la 7:1 și 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;
}
}
Testare automată: axe-core, MAUVE++ și Lighthouse
Testarea automată a accesibilității poate detecta aproximativ 30-40% dintre erorile WCAG, conform estimărilor Deque Systems. Restul de 60-70% necesită testare manuală cu cititoare de ecran reale. Cu toate acestea, automatizarea este esențială pentru a surprinde regresiile în ciclul 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())
Testare manuală cu cititor de ecran
Testarea manuală cu cititoare de ecran este de neînlocuit. Cititoarele de ecran principale de testat sunt:
- NVDA (NonVisual Desktop Access) + Chrome/Firefox: cel mai folosit pe Windows, gratuit, open source. Combinație implicită în testele AGID.
- FĂCILE (Job Access With Speech) + Chrome: cel mai popular cititor de ecran profesional, contra cost. Folosit de mulți utilizatori profesioniști.
- VoiceOver + Safari: pe macOS și iOS. Test esențial pentru dispozitivele Apple.
- TalkBack + Chrome: cititor de ecran nativ Android. Fundamental pentru serviciile mobile.
Lista de verificare a testării manuale cu cititor de ecran
- Navigați în întreaga pagină numai cu Tab: fiecare element interactiv trebuie să fie accesibil și utilizabil
- Verificați dacă titlul paginii este anunțat la aterizare (WCAG 2.4.2)
- Testați formularele: fiecare etichetă trebuie citită înainte de câmp, erorile trebuie anunțate
- Testarea modalelor: concentrarea ar trebui să se mute la modal la deschidere și înapoi la declanșatorul la închidere
- Verificați structura titlurilor: trebuie să fie logică și să nu săriți peste niveluri (h1 > h2 > h3)
- Tabelele de testare: anteturile de rând și de coloană trebuie să fie asociate cu celule (domeniu, id/anteturi)
- Verificați linkurile: „Click here” este inaccesibil; textul linkului trebuie să descrie destinația
- Testați notificările live: aria-live="polite" pentru actualizări non-urgente, "asertive" pentru erori critice
Declarația de accesibilitate AGID
Fiecare AP trebuie să publice pe site-ul său web a Declarație de accesibilitate conformă la structura definită de AGID (modelul european). Declarația trebuie să includă: starea de conformitate (conform / parțial conform / neconform), conținut neaccesibil cu motivul, contacte pt rapoarte, link către mecanismul de raportare AGID și data ultimei actualizări.
Țintele de accesibilitate trebuie publicate anual de către 31 martie, așa cum este prevăzut în Planul trienal TIC 2024-2026. Nepublicarea sau neactualizarea declarației este sancționată de AGID.
Concluzii și pașii următori
Accesibilitatea WCAG 2.1 AA nu este doar o cerință de reglementare: este un indicator al calității a codului și grija pentru utilizatori. O interfață accesibilă funcționează mai bine pentru toată lumea: navigarea tastatura ajută utilizatorii cu putere, contrastele ridicate îmbunătățesc lizibilitatea în condiții luminoase textele dificile, alternative îmbunătățesc SEO.
Odată cu intrarea în vigoare a Actului european de accesibilitate în iunie 2025, accesibilitatea devine obligatoriu și pentru companiile private care oferă servicii digitale, realizând competențele descrise în acest articol și mai valoros pe piață.
Articole similare din această serie
- GovTech #03: Open Data API Design - publicați și consumați date publice
- GovTech #04: GDPR-by-Design - modele arhitecturale pentru serviciile publice
- GovTech #06: Integrare API guvernamentală - SPID, CIE și pagoPA







