PA のアクセス可能な UI: WCAG 2.1 AA 実装
イタリア行政向けにアクセシブルなユーザー インターフェイスを実装する方法: WCAG 2.1 AA、 スクリーン リーダー、キーボード ナビゲーション、MAUVE++ と axe-core による自動テスト、およびコンプライアンス デジタル アクセシビリティに関するイタリアの法律 (Legge Stanca および AGID)。
PA におけるデジタル アクセシビリティ: オプションではなく義務
イタリア行政のウェブサイトとモバイルアプリケーションのアクセシビリティは次のとおりです。 によって統治される 法律 2004 年 1 月 9 日、n. 4 (法スタンカ)、更新者 立法令 106/2018 ウェブサイトのアクセシビリティに関する EU 指令 2016/2102 を施行しました。 公共部門の団体。参考となる技術ガイドラインは次のとおりです。 WCAG 2.1 (Web コンテンツ アクセシビリティ ガイドライン) コンプライアンス レベル AA.
2020 年 9 月 23 日以降、すべての PA は アクセシビリティに関する声明 ウェブサイトで毎年更新されます。 AgID は自動システムを通じてコンプライアンスを監視します モーヴ++ (CNR で開発) HTML および CSS コードを 50 基準中 31 基準に基づいて分析します WCAG 2.1 レベル A および AA に成功しました。
から 2025 年 6 月 28 日 に発効します 欧州アクセシビリティ法 (EAA)、 EU 指令 2019/882、アクセシビリティ義務をデジタル製品およびサービスにも拡大 個人: 電子商取引、銀行、交通サービス、メディア。 PA にとって、EAA は既存の義務を強化します。
何を学ぶか
- WCAG (POUR) の 4 つの原則と PA の最も重要な成功基準
- セマンティック HTML: アクセシブルな実装に不可欠な基盤
- ARIA (Accessible Rich Internet Applications): 使用する場合と使用しない場合
- キーボード ナビゲーション: フォーカス管理、スキップ リンク、および表示されるフォーカス モード
- スクリーン リーダー: VoiceOver、NVDA、JAWS が DOM を解釈する方法
- 自動テスト: axe-core、MAUVE++、Lighthouse、Pa11y
- 手動テスト: 実際のスクリーン リーダーを使用したシナリオ
- AGID アクセシビリティ宣言: 構造と発行
WCAG の 4 つの原則: 注ぐ
WCAG 2.1 は、頭字語で知られる 4 つの基本原則に基づいています。 注ぐ:
| 原理 | 説明 | 主要な基準 (AA) | 一般的な PA エラー |
|---|---|---|---|
| 知覚可能 | 情報は知覚可能な方法で提示されなければなりません | 代替テキスト、4.5:1 コントラスト、キャプション、音声説明 | alt のない画像、PDF にアクセスできない、字幕のないビデオ |
| 操作可能 | すべてがキーボードから十分な時間内に使用できるようにする必要があります | キーボードでアクセス可能、リンクをスキップ、発作を誘発するコンテンツなし | マウスのみのメニュー、タイムアウトが短すぎる、フォーカスが表示されない |
| 理解できる | コンテンツとインターフェイスは理解できるものでなければなりません | ページ言語、フォームラベル、エラー防止 | ラベルのないフォーム、説明的でないエラー、言語の欠落 |
| 屈強 | コンテンツは現在および将来の支援技術で解釈可能でなければなりません | 有効な HTML 解析、すべてのコンポーネントの名前/役割/値 | 無効な HTML、不適切に使用された ARIA、ロールのないカスタム コンポーネント |
セマンティック HTML: すべての基礎
Web アクセシビリティの最初のルールは、正しいセマンティック HTML を使用することです。 ARIAを適用する前に、
ネイティブ HTML 要素がすでに正しい構造と役割を HTML 要素に伝えていることを確認してください。
スクリーンリーダー。セマンティック HTML は本質的にアクセス可能です。問題は使用時に発生します
一般的な要素 (<div>, <span>) HTML の機能用
ネイティブの方がより適切に処理します。
<!-- 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. -->
アクセシブルなフォーム: PA サービスの重要なポイント
オンライン フォームは、書類の請求、登録、申請など、市民と PA の間のやり取りの中心です。 申告、支払い。アクセシビリティに関するエラーが最も発生する場所でもあります 頻度が高く、より影響力のあるものになります。アクセシブルでないフォームは障害のある国民を事実上排除する 公共サービスへのアクセスから。
<!-- 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: いつ使用するか (そしていつ使用を避けるべきか)
ARIA (Accessible Rich Internet Applications) は、役割、状態、プロパティを伝達する属性を使用して HTML を拡張します。 支援技術まで。 ARIA の最初のルールには次のように記載されています。ネイティブ HTML 要素を使用できる場合は、 要素を再利用して ARIA ロールを追加する代わりに、必要なセマンティクスと動作がすでに組み込まれている、 状態またはプロパティにアクセスできるようにしてから、そうします".
AIRが欠かせないものとなる カスタムウィジェット ネイティブ HTML では次のことはカバーされていません。 モーダル、アコーディオン、タブ、スライダー、オートコンプリート付きコンボボックス。このような場合に役立つのが ARIA です。 セマンティクスをスクリーン リーダーに伝えるのは正しいです。
<!-- 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>
コントラストとタイポグラフィー: 知覚可能な基準
WCAG 2.1 基準 1.4.3 コントラスト(最小) AAレベルではコントラスト比が必要です 少なくとも 4.5:1 プレーンテキスト e の場合 3:1 大きなテキストの場合 (18pt または 14pt の太字) および UI コンポーネント (入力枠、情報アイコン)。基準 1.4.6 コントラスト (強化) AAA レベルでは、これらのしきい値は 7:1 と 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;
}
}
自動テスト: axe-core、MAUVE++、Lighthouse
自動アクセシビリティ テストでは、おおよそのことを検出できます。 WCAG エラーの 30 ~ 40%、 Deque Systems の推定によると。残りの 60 ~ 70% については、実際のスクリーン リーダーを使用した手動テストが必要です。ただし、 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())
スクリーンリーダーを使用した手動テスト
スクリーン リーダーを使用した手動テストは、かけがえのないものです。テストする主なスクリーン リーダーは次のとおりです。
- NVDA (非ビジュアル デスクトップ アクセス) + Chrome/Firefox: Windows で最もよく使用されます。 無料のオープンソース。 AGID テストのデフォルトの組み合わせ。
- ジョーズ (音声によるジョブ アクセス) + Chrome: 最も人気のあるプロ仕様のスクリーン リーダー、 有料です。多くのプロユーザーに愛用されています。
- ナレーション + Safari: macOS および iOS 上。 Apple デバイスの必須テスト。
- トークバック + Chrome: Android ネイティブ スクリーン リーダー。モバイル サービスの基本。
スクリーン リーダーを使用した手動テスト チェックリスト
- タブのみを使用してページ全体を移動します。すべてのインタラクティブな要素がアクセス可能で使用可能である必要があります。
- ページタイトルがランディング時にアナウンスされることを確認する (WCAG 2.4.2)
- フォームをテストします。フィールドの前に各ラベルを読み取る必要があり、エラーをアナウンスする必要があります。
- モーダルのテスト: 開くとモーダルにフォーカスが移動し、閉じるとトリガーに戻る必要があります。
- 見出しの構造を確認してください。見出しの構造は論理的である必要があり、レベルをスキップしてはなりません (h1 > h2 > h3)
- テストテーブル: 行ヘッダーと列ヘッダーはセル (スコープ、ID/ヘッダー) に関連付ける必要があります。
- リンクを確認してください。「ここをクリック」にアクセスできません。リンクテキストはリンク先を説明する必要があります
- ライブ通知をテストします。緊急でない更新の場合は aria-live="polite"、重大なエラーの場合は "assertive"
AGID アクセシビリティ宣言
各 PA は Web サイトで次の内容を公開する必要があります。 アクセシビリティに関する声明 準拠した AGID (欧州モデル) によって定義された構造に準拠します。声明には以下を含める必要があります: コンプライアンスステータス (対応/一部対応/非対応)、理由によりアクセスできない内容、お問い合わせ先 レポート、AGID レポート メカニズムへのリンク、および最終更新日。
アクセシビリティ目標は、毎年次の機関によって公表されなければなりません。 3月31日、 ICT 3 か年計画 2024 ~ 2026 で想定されているとおりです。公開できない、または宣言が更新されていない AGID によって認可されています。
結論と次のステップ
WCAG 2.1 AA のアクセシビリティは単なる規制要件ではなく、品質の指標です コードの見直しとユーザーへの配慮。アクセシブルなインターフェースは誰にとってもより効果的に機能します: ナビゲーション キーボードはパワー ユーザーに役立ち、ハイ コントラストにより明るい場所での可読性が向上します 難しい代替テキストは SEO を向上させます。
2025 年 6 月の欧州アクセシビリティ法の発効により、アクセシビリティは デジタルサービスを提供する民間企業にも義務付けられており、記載されているスキルが必要となります。 この記事では、市場でさらに価値のあるものを紹介します。
このシリーズの関連記事
- ガバメントテック #03: オープン データ API 設計 - パブリック データの公開と利用
- ガバメントテック #04: GDPR-by-Design - 公共サービスのアーキテクチャ パターン
- ガバメントテック #06: 政府 API の統合 - SPID、CIE、pagoPA







