PA için Erişilebilir Kullanıcı Arayüzü: WCAG 2.1 AA uygulaması
İtalyan Kamu Yönetimi için erişilebilir kullanıcı arayüzleri nasıl uygulanır: WCAG 2.1 AA, ekran okuyucu, klavyeyle gezinme, MAUVE++ ve axe-core ile otomatik test ve uyumluluk Dijital erişilebilirliğe ilişkin İtalyan mevzuatına (Legge Stanca ve AGID).
KA'da Dijital Erişilebilirlik: Seçenek Değil, Zorunluluk
İtalyan Kamu Yönetiminin web sitelerinin ve mobil uygulamalarının erişilebilirliği tarafından yönetiliyor Kanun 9 Ocak 2004, n. 4 (Law Stanca), tarafından güncellendi Kanun Hükmünde Kararname 106/2018 Web sitelerinin erişilebilirliğine ilişkin 2016/2102 sayılı AB Direktifini uygulayan kamu sektörü organları. Referans teknik yönergeler şunlardır: WCAG 2.1 (Web İçeriği Erişilebilirlik Yönergeleri) uyumluluk düzeyinde AA.
23 Eylül 2020'den itibaren tüm KA'lar bir Erişilebilirlik Beyanı her yıl güncellenen web sitesinde yer almaktadır. AgID, otomatik sistem aracılığıyla uyumluluğu izler leylak rengi++ (CNR ile geliştirilmiştir) HTML ve CSS kodunu 50 kriterden 31'ine göre analiz eder başarılı WCAG 2.1 seviye A ve AA.
itibaren 28 Haziran 2025 tarihinde yürürlüğe giriyor Avrupa Erişilebilirlik Yasası (EAA), Erişilebilirlik yükümlülüğünü dijital ürün ve hizmetleri de kapsayacak şekilde genişleten 2019/882 sayılı AB Direktifi özel kişiler: e-ticaret, bankacılık, ulaşım hizmetleri, medya. PA için EAA mevcut yükümlülükleri güçlendiriyor.
Ne Öğreneceksiniz
- PA için 4 WCAG (POUR) ilkesi ve en kritik başarı kriterleri
- Semantik HTML: Erişilebilir her uygulamanın vazgeçilmez temeli
- ARIA (Erişilebilir Zengin İnternet Uygulamaları): ne zaman kullanılmalı ve ne zaman kaçınılmalı
- Klavye navigasyonu: odak yönetimi, atlama bağlantıları ve görünür odak modları
- Ekran okuyucular: VoiceOver, NVDA ve JAWS, DOM'u nasıl yorumluyor?
- Otomatik test: axe-core, MAUVE++, Lighthouse, Pa11y
- Manuel test: gerçek ekran okuyucularla senaryolar
- AGID Erişilebilirlik Bildirgesi: yapı ve yayın
4 WCAG İlkesi: DÖKÜN
WCAG 2.1, kısaltmayla bilinen 4 temel prensibe dayanmaktadır. DÖKÜN:
| Prensip | Tanım | Temel Kriterler (AA) | Yaygın PA Hataları |
|---|---|---|---|
| algılanabilir | Bilgi algılanabilir şekillerde sunulabilir olmalıdır | Alternatif metin, 4,5:1 kontrast, altyazılar, sesli açıklamalar | Altyazısız görseller, PDF'lere erişilemiyor, altyazısız videolar |
| Çalıştırılabilir | Her şey klavyeden ve yeterli sürede kullanılabilir olmalıdır | Klavyeyle erişilebilir, bağlantıları atla, nöbete neden olacak içerik yok | Yalnızca fare menüsü, zaman aşımları çok kısa, odak görünmüyor |
| Anlaşılabilir | İçerik ve arayüz anlaşılır olmalı | Sayfa dili, form etiketleri, hata önleme | Etiketsiz form, açıklayıcı olmayan hatalar, eksik dil |
| Sağlam | İçerik mevcut ve gelecekteki yardımcı teknolojiler tarafından yorumlanabilir olmalıdır. | Geçerli HTML ayrıştırma, tüm bileşenler için ad/rol/değer | Geçersiz HTML, kötü kullanılmış ARIA, rolü olmayan özel bileşenler |
Anlamsal HTML: her şeyin temeli
Web erişilebilirliğinin ilk kuralı doğru anlamsal HTML kullanmaktır. ARIA'yı uygulamadan önce,
yerel HTML öğelerinin halihazırda doğru yapıyı ve rolü kullanıcıya ilettiğinden emin olun.
ekran okuyucular. Anlamsal HTML doğası gereği erişilebilirdir; sorun kullanıldıkça ortaya çıkıyor
genel öğeler (<div>, <span>) HTML'nin işlevselliği için
yerli daha iyi idare eder.
<!-- 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. -->
Erişilebilir Formlar: KA Hizmetlerinde Kritik Nokta
Çevrimiçi formlar vatandaşlar ve KA arasındaki etkileşimin kalbidir: belge talebi, kayıtlar, beyanlar, ödemeler. Ayrıca erişilebilirlik hatalarının en fazla olduğu yerlerdir daha sık ve daha etkili. Erişilemeyen bir form, engelli vatandaşları etkili bir şekilde dışlıyor kamu hizmetlerine erişimden.
<!-- 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>
HAVA: Ne Zaman Kullanılmalı (ve Ne Zaman Kaçınılmalı)
ARIA (Erişilebilir Zengin İnternet Uygulamaları), HTML'yi rolleri, durumları ve özellikleri ileten niteliklerle genişletir yardımcı teknolojilere. ARIA'nın ilk kuralı şöyle diyor: "Yerel bir HTML öğesini aşağıdakilerle birlikte kullanabiliyorsanız: Bir öğeyi yeniden amaçlandırmak ve bir ARIA rolü eklemek yerine, zaten yerleşik olarak ihtiyaç duyduğunuz anlam ve davranış, erişilebilir kılmak için devlet veya mülk, sonra bunu yapın".
HAVA vazgeçilmez hale geliyor özel widget'lar yerel HTML şunları kapsamaz: modeller, akordeon, sekmeler, kaydırıcı, otomatik tamamlama özelliğine sahip açılan kutu. Bu durumlarda ARIA bir araçtır Anlambilimi ekran okuyucuya iletmek için doğru.
<!-- 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 ve Tipografi: Algılanabilir Kriterler
WCAG 2.1 kriteri 1.4.3 Kontrast (Minimum) AA düzeyinde bir kontrast oranı gerekir en azından 4.5:1 düz metin için e 3:1 büyük metinler için (18pt veya 14pt kalın) ve UI bileşenleri (giriş sınırları, bilgi simgeleri). Kriter 1.4.6 Kontrast (Geliştirilmiş) AAA düzeyinde bu eşikleri 7:1 ve 4,5:1'e getirir.
/* 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;
}
}
Otomatik Test: axe-core, MAUVE++ ve Lighthouse
Otomatik erişilebilirlik testi yaklaşık olarak WCAG hatalarının %30-40'ı, Deque Systems'in tahminlerine göre. Geriye kalan %60-70'lik kısım, gerçek ekran okuyucularla manuel test yapılmasını gerektirir. Ancak, CI/CD döngüsündeki gerilemeleri yakalamak için otomasyon şarttır.
# 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())
Ekran Okuyucuyla Manuel Test
Ekran okuyucularla manuel testin yeri doldurulamaz. Test edilecek ana ekran okuyucular şunlardır:
- NVDA (Görsel Olmayan Masaüstü Erişimi) + Chrome/Firefox: Windows'ta en çok kullanılan, ücretsiz, açık kaynak. AGID testlerinde varsayılan kombinasyon.
- ÇENELER (Konuşmayla İş Erişimi) + Chrome: en popüler profesyonel ekran okuyucu, bir ücret karşılığında. Birçok profesyonel kullanıcı tarafından kullanılmaktadır.
- Seslendirme + Safari: macOS ve iOS'ta. Apple cihazları için temel test.
- TalkBack + Chrome: Android yerel ekran okuyucusu. Mobil hizmetler için temel.
Ekran Okuyuculu Manuel Test Kontrol Listesi
- Tüm sayfada yalnızca Sekme ile gezinin: her etkileşimli öğe ulaşılabilir ve kullanılabilir olmalıdır
- Sayfa başlığının açılışta duyurulduğunu kontrol edin (WCAG 2.4.2)
- Formları test edin: Her etiket alandan önce okunmalı, hatalar bildirilmelidir
- Test modları: odak, açılışta modala ve kapanışta tetiğe geri dönmeli
- Başlık yapısını kontrol edin: mantıklı olmalı ve seviyeleri atlamamalıdır (h1 > h2 > h3)
- Test tabloları: satır ve sütun başlıkları hücrelerle ilişkilendirilmelidir (kapsam, kimlik/başlıklar)
- Bağlantıları kontrol edin: "Buraya tıklayın" erişilemiyor; bağlantı metni hedefi tanımlamalıdır
- Canlı bildirimleri test edin: acil olmayan güncellemeler için aria-live = "polite", kritik hatalar için "iddialı"
AGID Erişilebilirlik Beyanı
Her KA kendi web sitesinde bir Erişilebilirlik Beyanı uyumlu AGID (Avrupa modeli) tarafından tanımlanan yapıya. Beyanın şunları içermesi gerekir: uyumluluk durumu (uyumlu/kısmen uyumlu/uyumsuz), içeriklere gerekçesi ile erişilemiyor, iletişim bilgileri raporlar, AGID raporlama mekanizmasına bağlantı ve son güncelleme tarihi.
Erişilebilirlik hedefleri yıllık olarak yayınlanmalıdır. 31 Mart, 2024-2026 Üç Yıllık BİT Planı'nda öngörüldüğü gibi. Yayımlanamaması veya beyanın güncellenmemesi AGID tarafından yaptırıma tabidir.
Sonuçlar ve Sonraki Adımlar
WCAG 2.1 AA erişilebilirliği yalnızca düzenleyici bir gereklilik değildir: kalitenin bir göstergesidir Kod ve kullanıcılar için bakım. Erişilebilir bir arayüz herkes için daha iyi çalışır: gezinme klavye uzman kullanıcılara yardımcı olur, yüksek kontrastlar parlak koşullarda okunabilirliği artırır zor, alternatif metinler SEO'yu geliştirir.
Avrupa Erişilebilirlik Yasası'nın Haziran 2025'te yürürlüğe girmesiyle erişilebilirlik, açıklanan becerilerin sağlanması, dijital hizmetler sunan özel şirketler için de zorunludur bu yazıda piyasada daha da değerli.
Bu Serideki İlgili Makaleler
- Devlet Teknolojisi #03: Açık Veri API Tasarımı - genel verileri yayınlayın ve kullanın
- Devlet Teknolojisi #04: GDPR-by-Design - kamu hizmetlerine yönelik mimari modeller
- Devlet Teknolojisi #06: Devlet API entegrasyonu - SPID, CIE ve pagoPA







