Web Accessibility

Π’Π΅Π±-Π΄ΠΎΡΡ‚ΡƒΠΏΠ½ΠΎΡΡ‚ΡŒ (a11y) β€” ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠΈ, Π΄Π΅Π»Π°ΡŽΡ‰ΠΈΠ΅ сайт ΠΏΡ€ΠΈΠ³ΠΎΠ΄Π½Ρ‹ΠΌ для людСй с ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½Π½Ρ‹ΠΌΠΈ возмоТностями: нСзрячих, слабовидящих, людСй с ΠΌΠΎΡ‚ΠΎΡ€Π½Ρ‹ΠΌΠΈ Π½Π°Ρ€ΡƒΡˆΠ΅Π½ΠΈΡΠΌΠΈ, ΠΊΠΎΠ³Π½ΠΈΡ‚ΠΈΠ²Π½Ρ‹ΠΌΠΈ особСнностями.

Основной стандарт β€” WCAG 2.2 (Web Content Accessibility Guidelines). Π£Ρ€ΠΎΠ²Π΅Π½ΡŒ AA β€” ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ для коммСрчСских сайтов.

WCAG 2.2 AA β€” ΠΊΠ»ΡŽΡ‡Π΅Π²Ρ‹Π΅ трСбования

Perceivable (Π²ΠΎΡΠΏΡ€ΠΈΠ½ΠΈΠΌΠ°Π΅ΠΌΠΎΡΡ‚ΡŒ)

  • ВсС изобраТСния ΠΈΠΌΠ΅ΡŽΡ‚ осмыслСнный alt (Π½Π΅ β€œimage”, Π½Π΅ пустой для ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΠ²Π½Ρ‹Ρ…)
  • Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΈΠ²Π½Ρ‹Π΅ изобраТСния: alt="" ΠΈΠ»ΠΈ role="presentation"
  • Π’ΠΈΠ΄Π΅ΠΎ ΠΈ Π°ΡƒΠ΄ΠΈΠΎ: субтитры ΠΈΠ»ΠΈ тСкстовая Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π°
  • ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π½Π΅ зависит Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΡ‚ Ρ†Π²Π΅Ρ‚Π° (1.4.1)
  • ВСкст ΠΌΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΡƒΠ΅Ρ‚ΡΡ Π΄ΠΎ 200% Π±Π΅Π· ΠΏΠΎΡ‚Π΅Ρ€ΠΈ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ (1.4.4)
  • Reflow: ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ Ρ‡ΠΈΡ‚Π°Π΅ΠΌ ΠΏΡ€ΠΈ ΡˆΠΈΡ€ΠΈΠ½Π΅ 320 CSS px Π±Π΅Π· Π³ΠΎΡ€ΠΈΠ·ΠΎΠ½Ρ‚Π°Π»ΡŒΠ½ΠΎΠΉ ΠΏΡ€ΠΎΠΊΡ€ΡƒΡ‚ΠΊΠΈ (1.4.10)
  • ΠšΠΎΠ½Ρ‚Ρ€Π°ΡΡ‚ тСкста ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ 4.5:1, ΠΊΡ€ΡƒΠΏΠ½ΠΎΠ³ΠΎ тСкста 3:1 (1.4.3)
  • UI-ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ ΠΈ графичСскиС ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹: контраст ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ 3:1 (1.4.11)

Operable (ΡƒΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌΠΎΡΡ‚ΡŒ)

  • ВсС ΠΈΠ½Ρ‚Π΅Ρ€Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Π΅ элСмСнты доступны с ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Ρ‹ (2.1.1)
  • НСт ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π½Ρ‹Ρ… Π»ΠΎΠ²ΡƒΡˆΠ΅ΠΊ (2.1.2)
  • Skip-link β€œΠŸΠ΅Ρ€Π΅ΠΉΡ‚ΠΈ ΠΊ содСрТимому” β€” ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ фокусируСмый элСмСнт (2.4.1)
  • Π£Π½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ <title> для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Π° (2.4.2)
  • Π›ΠΎΠ³ΠΈΡ‡Π½Ρ‹ΠΉ порядок фокуса (2.4.3)
  • [2.2] Focus Not Obscured (2.4.11): элСмСнт ΠΏΡ€ΠΈ фокусС Π½Π΅ ΠΏΠ΅Ρ€Π΅ΠΊΡ€Ρ‹Ρ‚ sticky-шапкой
  • [2.2] Dragging Movements (2.5.7): для drag-ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ Π΅ΡΡ‚ΡŒ Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π° Ρ‡Π΅Ρ€Π΅Π· ΠΊΠ»ΠΈΠΊ
  • [2.2] Target Size Minimum (2.5.8): ΠΈΠ½Ρ‚Π΅Ρ€Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Π΅ элСмСнты ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ 24x24 CSS px

Understandable (ΠΏΠΎΠ½ΡΡ‚Π½ΠΎΡΡ‚ΡŒ)

  • <html lang="ru"> (3.1.1)
  • Π€Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚Ρ‹ Π½Π° Π΄Ρ€ΡƒΠ³ΠΎΠΌ языкС ΠΎΠ±Ρ‘Ρ€Π½ΡƒΡ‚Ρ‹ <span lang="en"> (3.1.2)
  • Ошибки Ρ„ΠΎΡ€ΠΌ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Ρ‹ тСкстом, Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ†Π²Π΅Ρ‚ΠΎΠΌ (3.3.1)
  • Поля Ρ„ΠΎΡ€ΠΌ ΠΈΠΌΠ΅ΡŽΡ‚ Π²ΠΈΠ΄ΠΈΠΌΡ‹Π΅ labels (3.3.2)
  • [2.2] Accessible Authentication (3.3.8): авторизация Π±Π΅Π· ΠΊΠΎΠ³Π½ΠΈΡ‚ΠΈΠ²Π½Ρ‹Ρ… тСстов

Robust (Π½Π°Π΄Ρ‘ΠΆΠ½ΠΎΡΡ‚ΡŒ)

  • Π’Π°Π»ΠΈΠ΄Π½Ρ‹ΠΉ HTML β€” Π½Π΅Ρ‚ Π΄ΡƒΠ±Π»ΠΈΡ€ΡƒΡŽΡ‰ΠΈΡ… id (4.1.1)
  • ΠšΠ°ΡΡ‚ΠΎΠΌΠ½Ρ‹Π΅ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ ΠΈΠΌΠ΅ΡŽΡ‚ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½Ρ‹Π΅ ARIA-Ρ€ΠΎΠ»ΠΈ (4.1.2)
  • БтатусныС сообщСния Ρ‡Π΅Ρ€Π΅Π· role="status" ΠΈΠ»ΠΈ aria-live (4.1.3)

ARIA-ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ для SPA

  • Live regions (aria-live="polite") Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π² DOM ΠΏΡ€ΠΈ Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ΅, Π½Π΅ ΡΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒΡΡ динамичСски
  • aria-expanded Π½Π° ΠΊΠ½ΠΎΠΏΠΊΠ°Ρ… dropdown/accordion
  • aria-current="page" Π½Π° Π°ΠΊΡ‚ΠΈΠ²Π½ΠΎΠΌ ΠΏΡƒΠ½ΠΊΡ‚Π΅ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ
  • aria-label Π½Π° элСмСнтах Π±Π΅Π· Π²ΠΈΠ΄ΠΈΠΌΠΎΠ³ΠΎ тСкста (icon-only ΠΊΠ½ΠΎΠΏΠΊΠΈ)
  • aria-describedby связываСт hint-тСкст ΠΈ ошибки с полями Ρ„ΠΎΡ€ΠΌ
  • aria-hidden="true" Π½Π° Π΄Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΈΠ²Π½Ρ‹Ρ… SVG-ΠΈΠΊΠΎΠ½ΠΊΠ°Ρ…
  • НС ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ARIA Π³Π΄Π΅ достаточно сСмантичСского HTML

Vue 3 SPA β€” спСцифика

ОбъявлСниС смСны ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Π°

SPA мСняСт страницу Π±Π΅Π· ΠΏΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ β€” скринридСр ΠΌΠΎΠ»Ρ‡ΠΈΡ‚. РСшСниС:

// router.ts
router.afterEach((to) => {
  document.title = to.meta.title as string || 'SiteName';
  nextTick(() => {
    const heading = document.querySelector('h1');
    if (heading) {
      heading.setAttribute('tabindex', '-1');
      heading.focus();
    }
    const announcer = document.getElementById('route-announcer');
    if (announcer) announcer.textContent = to.meta.title as string;
  });
});
<!-- App.vue β€” всСгда Π² DOM -->
<div id="route-announcer" aria-live="polite" aria-atomic="true" class="sr-only"></div>

ДинамичСский ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚

  • Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° Π΄Π°Π½Π½Ρ‹Ρ…: aria-busy="true" Π½Π° ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π΅
  • v-if/v-show для live region: ΠΏΡ€Π΅Π΄ΠΏΠΎΡ‡ΠΈΡ‚Π°Ρ‚ΡŒ v-show ΠΈΠ»ΠΈ Π΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€ Π² DOM
  • Toast-увСдомлСния Ρ‡Π΅Ρ€Π΅Π· live region, Π½Π΅ Ρ‡Π΅Ρ€Π΅Π· динамичСскоС созданиС DOM

Π”ΠΎΡΡ‚ΡƒΠΏΠ½ΠΎΡΡ‚ΡŒ Leaflet-ΠΊΠ°Ρ€Ρ‚

  • ΠšΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€: aria-label="Π˜Π½Ρ‚Π΅Ρ€Π°ΠΊΡ‚ΠΈΠ²Π½Π°Ρ ΠΊΠ°Ρ€Ρ‚Π° ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Π°" + role="application"
  • ΠœΠ°Ρ€ΠΊΠ΅Ρ€Ρ‹: L.marker([lat, lng], { alt: 'ОписаниС Ρ‚ΠΎΡ‡ΠΊΠΈ' })
  • Кнопки zoom: aria-label="ΠŸΡ€ΠΈΠ±Π»ΠΈΠ·ΠΈΡ‚ΡŒ", aria-label="ΠžΡ‚Π΄Π°Π»ΠΈΡ‚ΡŒ"
  • ВСкстовая Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π° ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Π° (список Ρ‚ΠΎΡ‡Π΅ΠΊ с адрСсами)
  • Если ΠΊΠ°Ρ€Ρ‚Π° дСкоративная: aria-hidden="true" + tabindex="-1"

Русскоязычная спСцифика

  • <html lang="ru"> ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎ β€” Π±Π΅Π· этого скринридСр произносит ΠΊΠΈΡ€ΠΈΠ»Π»ΠΈΡ†Ρƒ с английским ΠΏΡ€ΠΎΠΈΠ·Π½ΠΎΡˆΠ΅Π½ΠΈΠ΅ΠΌ
  • АнглоязычныС вставки: <span lang="en">Strava</span>
  • ARIA-labels Π½Π° русском: β€œΠžΡΠ½ΠΎΠ²Π½Π°Ρ навигация”, β€œΠ—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒβ€, β€œΠœΠ΅Π½ΡŽβ€
  • АббрСвиатуры: <abbr title="ΠΊΠΈΠ»ΠΎΠΌΠ΅Ρ‚Ρ€">ΠΊΠΌ</abbr>
  • ВСстированиС: NVDA с русским синтСзатором (eSpeak/RHVoice)

Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚Ρ‹ Π°ΡƒΠ΄ΠΈΡ‚Π°

АвтоматичСскиС (Π±Ρ€Π°ΡƒΠ·Π΅Ρ€)

Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚Π§Ρ‚ΠΎ Π΄Π΅Π»Π°Π΅Ρ‚
LighthouseChrome DevTools β†’ Lighthouse β†’ Accessibility. Score 0-100, ~50 ΠΏΡ€Π°Π²ΠΈΠ»
axe DevToolsΠ Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Chrome. ~100 ΠΏΡ€Π°Π²ΠΈΠ» axe-core, WCAG 2.0/2.1/2.2
WAVEwave.webaim.org ΠΈΠ»ΠΈ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅. Π’ΠΈΠ·ΡƒΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΠΎΠ²Π΅Ρ€Π»Π΅ΠΉ ΠΈΠΊΠΎΠ½ΠΎΠΊ Π½Π° страницС
Accessibility InsightsΠ Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΠ΅ Microsoft. Tab Stops β€” визуализация порядка фокуса

CI-интСграция

Playwright + axe-core (Ρ€Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡƒΠ΅ΠΌΡ‹ΠΉ):

npm install -D @playwright/test @axe-core/playwright
import AxeBuilder from '@axe-core/playwright';
test('page a11y', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .exclude('.leaflet-container')
    .analyze();
  expect(results.violations).toEqual([]);
});

ESLint-ΠΏΠ»Π°Π³ΠΈΠ½ (shift-left):

npm install -D eslint eslint-plugin-vuejs-accessibility vue-eslint-parser

pa11y (быстрый smoke test):

npx pa11y https://example.com --standard WCAG2AA

Π ΡƒΡ‡Π½ΠΎΠ΅ тСстированиС

VoiceOver (macOS): Cmd+F5, VO+U (Rotor) для headings/landmarks/links.

Keyboard: Tab/Shift+Tab ΠΏΠΎ всСй страницС. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ: Π²ΠΈΠ΄ΠΈΠΌΡ‹ΠΉ focus, Π»ΠΎΠ³ΠΈΡ‡Π½Ρ‹ΠΉ порядок, ΠΌΠΎΠ΄Π°Π»ΠΊΠΈ с focus trap.

Аудит RideHub (2026-04-03)

ΠžΠ±Ρ‰Π°Ρ ΠΎΡ†Π΅Π½ΠΊΠ°: 4/10. Базовая структура Π΅ΡΡ‚ΡŒ, Ρ†Π΅Π»Π΅Π½Π°ΠΏΡ€Π°Π²Π»Π΅Π½Π½ΠΎΠΉ Ρ€Π°Π±ΠΎΡ‚Ρ‹ Π½Π°Π΄ a11y Π½Π΅ Π±Ρ‹Π»ΠΎ.

Π§Ρ‚ΠΎ Ρ…ΠΎΡ€ΠΎΡˆΠΎ

  • <html lang="ru"> βœ“
  • БСмантичСскиС Ρ‚Π΅Π³ΠΈ <nav>, <main>, <footer>, <section> βœ“
  • document.title обновляСтся ΠΏΡ€ΠΈ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ (useSeo.ts) βœ“
  • prefers-reduced-motion Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½ βœ“
  • Dark mode с OS preference βœ“
  • Viewport Π½Π΅ Π±Π»ΠΎΠΊΠΈΡ€ΡƒΠ΅Ρ‚ zoom βœ“
  • Π¨Ρ€ΠΈΡ„Ρ‚Ρ‹ Π² rem/em βœ“
  • role="dialog" + aria-modal="true" Π½Π° ΠΌΠΎΠ΄Π°Π»ΠΊΠ°Ρ… βœ“

ΠšΡ€ΠΈΡ‚ΠΈΡ‡Π΅ΡΠΊΠΈΠ΅ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ (P0)

  1. Π€ΠΎΡ€ΠΌΡ‹ Π±Π΅Π· <label> β€” всС поля ΠΏΠΎΠ»Π°Π³Π°ΡŽΡ‚ΡΡ Π½Π° placeholder. LoginView, admin Ρ„ΠΎΡ€ΠΌΡ‹
  2. Focus styles ΡƒΠ½ΠΈΡ‡Ρ‚ΠΎΠΆΠ΅Π½Ρ‹ β€” outline: none Π±Π΅Π· Π·Π°ΠΌΠ΅Π½Ρ‹. НСт :focus-visible
  3. Ошибки Ρ„ΠΎΡ€ΠΌ Π½Π΅ ΠΎΠ·Π²ΡƒΡ‡ΠΈΠ²Π°ΡŽΡ‚ΡΡ β€” Π½Π΅Ρ‚ role="alert", Π½Π΅Ρ‚ aria-describedby
  4. НСт skip-link β€” ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π½Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΠΏΡ€ΠΎΡ…ΠΎΠ΄ΠΈΡ‚ всю Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΡŽ Π½Π° ΠΊΠ°ΠΆΠ΄ΠΎΠΉ страницС
  5. НСт route announcer β€” SPA мСняСт страницу, скринридСр ΠΌΠΎΠ»Ρ‡ΠΈΡ‚

Π‘Π΅Ρ€ΡŒΡ‘Π·Π½Ρ‹Π΅ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ (P1)

  1. Модалки Π±Π΅Π· focus trap β€” фокус ΡƒΡ…ΠΎΠ΄ΠΈΡ‚ Π·Π° ΠΏΡ€Π΅Π΄Π΅Π»Ρ‹. НСт Π²ΠΎΠ·Π²Ρ€Π°Ρ‚Π° фокуса Π½Π° Ρ‚Ρ€ΠΈΠ³Π³Π΅Ρ€
  2. ΠšΠ°Ρ€Ρ‚Ρ‹ нСдоступны β€” 14 Leaflet-ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ² Π±Π΅Π· aria-label
  3. <a> вмСсто <button> β€” LoginView logout, HomeView region selection
  4. Loading states β€” LoadingSpinner Π±Π΅Π· aria-live, aria-busy
  5. Низкий контраст β€” footer legal тСкст ~2.5:1 (Π½ΡƒΠΆΠ½ΠΎ 4.5:1)

План исправлСния

Π€Π°Π·Π° 1 (1 дСнь, quick wins): skip-link, labels Π½Π° Ρ„ΠΎΡ€ΠΌΡ‹, :focus-visible, role="alert" Π½Π° ошибки, route announcer.

Π€Π°Π·Π° 2 (2-3 дня): focus trap Π² ΠΌΠΎΠ΄Π°Π»ΠΊΠ°Ρ…, <a> β†’ <button>, aria-label Π½Π° ΠΊΠ°Ρ€Ρ‚Ρ‹, aria-hidden Π½Π° SVG.

Π€Π°Π·Π° 3 (3-5 Π΄Π½Π΅ΠΉ): контраст, aria-current/aria-expanded Π² Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ, axe-core Π² CI, ESLint-ΠΏΠ»Π°Π³ΠΈΠ½.

Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊΠΈ