PA의 디지털 접근성: 선택이 아닌 의무

이탈리아 공공행정 웹사이트 및 모바일 애플리케이션의 접근성은 다음과 같습니다. 에 의해 통치 법률 2004년 1월 9일, n. 4 (Law Stanca), 업데이트됨 입법령 2018년 106월 웹사이트 접근성에 대한 EU 지침 2016/2102를 구현했습니다. 공공 부문 기관. 참조 기술 지침은 다음과 같습니다. WCAG 2.1 (웹 콘텐츠 접근성 지침) 준수 수준 AA.

2020년 9월 23일부터 모든 PA는 접근성 선언문 웹사이트에서 매년 업데이트됩니다. AgID는 자동화된 시스템을 통해 규정 준수를 모니터링합니다. 자주빛++ (CNR로 개발) 50개 기준 중 31개 기준에 대해 HTML 및 CSS 코드를 분석합니다. 성공적인 WCAG 2.1 레벨 A 및 AA.

에서 2025년 6월 28일 에 발효된다 유럽 ​​접근성법(EAA), 디지털 제품 및 서비스에도 접근성 의무를 확대하는 EU 지침 2019/882 개인: 전자상거래, 은행, 운송 서비스, 미디어. PA의 경우 EAA는 ​​기존 의무를 강화합니다.

무엇을 배울 것인가

  • 4가지 WCAG(POUR) 원칙과 PA의 가장 중요한 성공 기준
  • 시맨틱 HTML: 접근 가능한 구현을 위한 필수 불가결한 기반
  • ARIA(Accessible Rich Internet Application): 언제 사용해야 하고 언제 피해야 할까요?
  • 키보드 탐색: 포커스 관리, 링크 건너뛰기 및 시각적 포커스 모드
  • 스크린 리더: VoiceOver, NVDA 및 JAWS가 DOM을 해석하는 방법
  • 자동화된 테스트: axe-core, MAUVE++, Lighthouse, Pa11y
  • 수동 테스트: 실제 화면 판독기를 사용한 시나리오
  • AGID 접근성 선언: 구조 및 출판

4가지 WCAG 원칙: POUR

WCAG 2.1은 두문자어로 알려진 4가지 기본 원칙을 기반으로 합니다. 붓다:

원칙 설명 주요 기준(AA) 일반적인 PA 오류
인지 가능 정보는 인지 가능한 방식으로 표현되어야 합니다. 대체 텍스트, 4.5:1 대비, 캡션, 오디오 설명 대체 없는 이미지, 액세스할 수 없는 PDF, 자막 없는 비디오
실시 가능한 모든 것을 키보드에서 충분한 시간 내에 사용할 수 있어야 합니다. 키보드 접근 가능, 링크 건너뛰기, 발작을 유발하는 콘텐츠 없음 마우스 전용 메뉴, 시간 초과가 너무 짧고 초점이 표시되지 않음
이해할 수 있는 콘텐츠와 인터페이스는 이해하기 쉬워야 합니다. 페이지 언어, 양식 라벨, 오류 방지 라벨이 없는 양식, 설명이 없는 오류, 언어 누락
건장한 콘텐츠는 현재 및 미래의 보조 기술로 해석 가능해야 합니다. 모든 구성 요소에 대한 유효한 HTML 구문 분석, 이름/역할/값 잘못된 HTML, 잘못 사용된 ARIA, 역할이 없는 맞춤 구성요소

시맨틱 HTML: 모든 것의 기초

웹 접근성의 첫 번째 규칙은 올바른 의미론적 HTML을 사용하는 것입니다. ARIA를 적용하기 전에, 기본 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 Application)는 역할, 상태 및 속성을 전달하는 속성으로 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())

스크린 리더를 사용한 수동 테스트

스크린 리더를 사용한 수동 테스트는 대체할 수 없습니다. 테스트할 기본 화면 판독기는 다음과 같습니다.

  • 엔비디아 (NonVisual Desktop Access) + Chrome/Firefox: Windows에서 가장 많이 사용되며, 무료, 오픈 소스. AGID 테스트의 기본 조합입니다.
  • 입 부분 (음성으로 작업 액세스) + Chrome: 가장 인기 있는 전문 스크린 리더, 유료로. 많은 전문 사용자가 사용합니다.
  • 보이스오버 + 사파리: macOS 및 iOS에서. Apple 장치에 대한 필수 테스트입니다.
  • TalkBack + Chrome: Android 기본 스크린 리더. 모바일 서비스의 기본입니다.

스크린 리더를 사용한 수동 테스트 체크리스트

  • Tab을 통해서만 전체 페이지를 탐색할 수 있습니다. 모든 대화형 요소는 접근 및 사용이 가능해야 합니다.
  • 랜딩 시 페이지 제목이 공지되는지 확인하세요(WCAG 2.4.2).
  • 양식 테스트: 각 라벨은 필드 전에 읽어야 하며 오류는 알려야 합니다.
  • 테스트 모달: 열 때 초점이 모달로 이동하고 닫을 때 트리거로 다시 이동해야 합니다.
  • 제목 구조를 확인하세요. 논리적이어야 하며 수준을 건너뛰어서는 안 됩니다(h1 > h2 > h3).
  • 테스트 테이블: 행 및 열 헤더는 셀(범위, ID/헤더)과 연결되어야 합니다.
  • 링크를 확인하세요. "여기를 클릭하세요"는 접근할 수 없습니다. 링크 텍스트는 대상을 설명해야 합니다.
  • 실시간 알림 테스트: 긴급하지 않은 업데이트의 경우 aria-live="polite", 심각한 오류의 경우 "assertive"

AGID 접근성 선언

각 PA는 웹 사이트에 다음을 게시해야 합니다. 접근성 선언문 준수 AGID(유럽 모델)에서 정의한 구조에 따릅니다. 명세서에는 다음이 포함되어야 합니다. 규정 준수 상태 (준수/일부 준수/비준수), 이유에 따른 내용 접근 불가, 연락처 보고서, AGID 보고 메커니즘 링크, 마지막 업데이트 날짜.

접근성 목표는 매년 게시되어야 합니다. 3월 31일, 2024~2026년 ICT 3개년 계획에 구상된 대로입니다. 게시 실패 또는 선언이 업데이트되지 않음 AGID의 승인을 받았습니다.

결론 및 다음 단계

WCAG 2.1 AA 접근성은 단순한 규제 요구사항이 아니라 품질의 지표입니다. 코드와 사용자 배려. 모든 사람에게 더 나은 접근성을 제공하는 인터페이스: 탐색 키보드는 고급 사용자에게 도움이 되며 고대비는 밝은 환경에서 가독성을 향상시킵니다. 어려운 대체 텍스트는 SEO를 향상시킵니다.

2025년 6월 유럽 접근성법(European Accessibility Act)이 발효되면서 접근성이 더욱 좋아졌습니다. 또한 디지털 서비스를 제공하는 민간 기업의 경우 필수 기술입니다. 이 기사에서는 시장에서 훨씬 더 가치가 있습니다.

이 시리즈의 관련 기사

  • 고브테크 #03: 개방형 데이터 API 설계 - 공공 데이터 게시 및 소비
  • 고브테크 #04: GDPR-by-Design - 공공 서비스를 위한 아키텍처 패턴
  • 고브테크 #06: 정부 API 통합 - SPID, CIE 및 pagoPA