Dostępny interfejs użytkownika dla PA: implementacja WCAG 2.1 AA
Jak wdrożyć dostępne interfejsy użytkownika dla włoskiej administracji publicznej: WCAG 2.1 AA, czytnik ekranu, nawigacja za pomocą klawiatury, automatyczne testowanie za pomocą MAUVE++ i axe-core oraz zgodność do włoskiego ustawodawstwa dotyczącego dostępności cyfrowej (Legge Stanca i AGID).
Dostępność cyfrowa w AP: obowiązek, a nie opcja
Dostępność stron internetowych i aplikacji mobilnych włoskiej administracji publicznej jest regulowane przez Ustawa z 9 stycznia 2004 r., n. 4 (Prawo Stanca), aktualizacja: Dekret legislacyjny 106/2018 która wdrożyła Dyrektywę UE 2016/2102 w sprawie dostępności stron internetowych organy sektora publicznego. Referencyjne wytyczne techniczne to: WCAG 2.1 (Wytyczne dotyczące dostępności treści internetowych) na poziomie zgodności AA.
Od 23 września 2020 r. wszystkie agencje płatnicze muszą opublikować: Oświadczenie o dostępności na swojej stronie internetowej, aktualizowanej co roku. AgID monitoruje zgodność za pośrednictwem zautomatyzowanego systemu MAŁO+ (opracowany wspólnie z CNR), który analizuje kod HTML i CSS pod kątem 31 z 50 kryteriów pomyślnie zdany egzamin WCAG 2.1 na poziomie A i AA.
Dal 28 czerwca 2025 r wchodzi w życie dnia Europejski akt o dostępności (EAA), Dyrektywa UE 2019/882, która rozszerza obowiązek dostępności również na produkty i usługi cyfrowe osoby prywatne: e-commerce, bankowość, usługi transportowe, media. W przypadku AP EAA wzmacnia istniejące zobowiązania.
Czego się nauczysz
- 4 zasady WCAG (POUR) i najważniejsze kryteria sukcesu PA
- Semantyczny HTML: niezbędna podstawa każdej dostępnej implementacji
- ARIA (Accessible Rich Internet Applications): kiedy z niej korzystać, a kiedy jej unikać
- Nawigacja za pomocą klawiatury: zarządzanie fokusem, pomijanie łączy i widoczne tryby fokusu
- Czytniki ekranu: Jak VoiceOver, NVDA i JAWS interpretują DOM
- Testowanie automatyczne: axe-core, MAUVE++, Lighthouse, Pa11y
- Testowanie ręczne: scenariusze z prawdziwymi czytnikami ekranu
- Deklaracja dostępności AGID: struktura i publikacja
4 zasady WCAG: WLEJ
WCAG 2.1 opiera się na 4 podstawowych zasadach, znanych pod skrótem WLAĆ:
| Zasada | Opis | Kluczowe kryteria (AA) | Najczęstsze błędy PA |
|---|---|---|---|
| Dostrzegalny | Informacje muszą być prezentowane w zauważalny sposób | Tekst alternatywny, kontrast 4,5:1, napisy, opisy audio | Obrazy bez altów, niedostępne pliki PDF, filmy bez napisów |
| Wykonalny | Wszystko musi być możliwe do użycia z klawiatury i w odpowiednim czasie | Dostępna za pomocą klawiatury, pomijaj linki, bez treści wywołujących ataki | Menu tylko myszy, limity czasu są zbyt krótkie, fokus nie jest widoczny |
| Zrozumiały | Treść i interfejs muszą być zrozumiałe | Język strony, etykiety formularzy, zapobieganie błędom | Formularz bez etykiety, błędy nieopisowe, brakujący język |
| Solidny | Treść musi nadawać się do interpretacji za pomocą obecnych i przyszłych technologii wspomagających | Prawidłowa analiza HTML, nazwa/rola/wartość dla wszystkich komponentów | Nieprawidłowy kod HTML, źle użyta ARIA, niestandardowe komponenty bez ról |
Semantyczny HTML: podstawa wszystkiego
Pierwszą zasadą dostępności sieci jest używanie poprawnego semantycznego kodu HTML. Przed zastosowaniem ARIA
upewnij się, że natywne elementy HTML przekazują już poprawną strukturę i rolę
czytniki ekranu. Semantyczny HTML jest z natury dostępny; problem pojawia się w momencie ich użycia
elementy ogólne (<div>, <span>) dla funkcjonalności HTML
natywny radzi sobie lepiej.
<!-- 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. -->
Dostępne formy: punkt krytyczny dla usług PA
Formularze internetowe stanowią serce interakcji między obywatelami a AP: wnioski o dokumenty, rejestracje, deklaracje, płatności. To tam występuje najwięcej błędów związanych z dostępnością częste i bardziej efektowne. Niedostępna forma skutecznie wyklucza obywateli niepełnosprawnych od dostępu do usług publicznych.
<!-- 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>
POWIETRZE: Kiedy go używać (a kiedy go unikać)
ARIA (Accessible Rich Internet Applications) rozszerza HTML o atrybuty, które komunikują role, stany i właściwości do technologii wspomagających. Pierwsza zasada ARIA stanowi: „Jeśli możesz użyć natywnego elementu HTML z semantyka i zachowanie, których potrzebujesz, są już wbudowane, zamiast zmieniać przeznaczenie elementu i dodawać rolę ARIA, stanu lub właściwości, aby były dostępne, a następnie zrób to".
AIR staje się niezbędny niestandardowe widżety ten natywny HTML nie obejmuje: modale, akordeon, tabulatory, slider, combobox z autouzupełnianiem. W takich przypadkach ARIA jest narzędziem poprawne przekazywanie semantyki do czytnika ekranu.
<!-- 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>
Kontrast i typografia: kryteria dostrzegalne
Kryterium WCAG 2.1 1.4.3 Kontrast (minimalny) na poziomie AA wymaga współczynnika kontrastu przynajmniej 4,5:1 dla zwykłego tekstu, np 3:1 w przypadku dużego tekstu (pogrubienie 18 pkt lub 14 pkt) oraz komponenty interfejsu użytkownika (obramowania wejściowe, ikony informacyjne). Kryterium 1.4.6 Kontrast (wzmocniony) na poziomie AAA podnosi te progi do 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;
}
}
Testowanie automatyczne: axe-core, MAUVE++ i Lighthouse
Zautomatyzowane testy dostępności mogą wykryć w przybliżeniu 30-40% błędów WCAG, według szacunków Deque Systems. Pozostałe 60-70% wymaga ręcznych testów z użyciem prawdziwych czytników ekranu. Jednakże, automatyzacja jest niezbędna do wychwytywania regresji w cyklu 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())
Testowanie ręczne za pomocą czytnika ekranu
Ręczne testowanie za pomocą czytników ekranu jest niezastąpione. Główne czytniki ekranu do przetestowania to:
- NVDA (NonVisual Desktop Access) + Chrome/Firefox: najczęściej używane w systemie Windows, darmowy, otwarty kod źródłowy. Domyślna kombinacja w testach AGID.
- SZCZĘKI (Job Access With Speech) + Chrome: najpopularniejszy profesjonalny czytnik ekranu, za opłatą. Używany przez wielu profesjonalnych użytkowników.
- VoiceOver + Safari: na macOS i iOS. Niezbędny test dla urządzeń Apple.
- TalkBack + Chrome: natywny czytnik ekranu dla Androida. Podstawa usług mobilnych.
Lista kontrolna testów ręcznych z czytnikiem ekranu
- Nawiguj po całej stronie tylko za pomocą Tab: każdy element interaktywny musi być osiągalny i użyteczny
- Sprawdź, czy tytuł strony jest ogłaszany przy lądowaniu (WCAG 2.4.2)
- Przetestuj formularze: każdą etykietę należy przeczytać przed polem, należy zgłosić błędy
- Testuj moduły: fokus powinien zostać przeniesiony na moduł modalny przy otwieraniu i z powrotem na spust przy zamykaniu
- Sprawdź strukturę nagłówka: musi być logiczna i nie może pomijać poziomów (h1 > h2 > h3)
- Tabele testowe: nagłówki wierszy i kolumn muszą być powiązane z komórkami (zakres, identyfikator/nagłówki)
- Sprawdź linki: „Kliknij tutaj” jest niedostępne; tekst linku musi opisywać miejsce docelowe
- Testuj powiadomienia na żywo: aria-live="polite" w przypadku niepilnych aktualizacji, "asertywny" w przypadku błędów krytycznych
Deklaracja dostępności AGID
Każda agencja płatnicza musi opublikować na swojej stronie internetowej: Oświadczenie o dostępności zgodny do struktury zdefiniowanej przez AGID (model europejski). Oświadczenie musi zawierać: status zgodności (zgodny / częściowo zgodny / niezgodny), treść niedostępna z przyczyny, kontakty do raporty, link do mechanizmu raportowania AGID oraz datę ostatniej aktualizacji.
Cele dostępności muszą być publikowane co roku przez 31 marca, zgodnie z planem trzyletnim ICT na lata 2024–2026. Brak publikacji lub brak aktualizacji oświadczenia podlega sankcjom AGID.
Wnioski i dalsze kroki
Dostępność WCAG 2.1 AA to nie tylko wymóg regulacyjny: to wskaźnik jakości kodu i dbałość o użytkowników. Przystępny interfejs działa lepiej dla wszystkich: nawigacja klawiatura pomaga zaawansowanym użytkownikom, wysoki kontrast poprawia czytelność w jasnych warunkach trudne, alternatywne teksty poprawiają SEO.
Wraz z wejściem w życie Europejskiego Aktu o Dostępności w czerwcu 2025 r. dostępność staje się obowiązkowe również dla prywatnych firm oferujących usługi cyfrowe, co oznacza, że umiejętności są opisane w tym artykule jeszcze bardziej wartościowe na rynku.
Powiązane artykuły z tej serii
- GovTech #03: Open Data API Design - publikuj i korzystaj z danych publicznych
- GovTech #04: RODO-by-Design - wzorce architektoniczne dla usług publicznych
- GovTech #06: Integracja API rządowych - SPID, CIE i pagoPA







