HTML & CSS

Доступність у CSS (CSS Accessibility)

Практичне керівництво з CSS-доступності: :focus-visible та кастомний outline, prefers-reduced-motion, prefers-color-scheme, forced-colors, sr-only техніка, контрастність кольорів, розміри touch targets та print styles.

Доступність у CSS

15% вашої аудиторії

Коли ми кажемо «доступність» (Accessibility, a11y), перша асоціація — сліпі користувачі зі скринридерами. Але реальність набагато ширша: 15% населення Землі мають ту чи іншу форму обмеження — моторну, зорову, слухову або когнітивну. Серед інших — користувачі, що тимчасово не можуть натиснути кнопку (зламана рука), люди похилого віку, або просто ті, хто дивиться контент на сонці без можливості збільшити яскравість.

CSS — один із головних інструментів доступності. Правильно написані стилі роблять інтерфейс читабельним при будь-яких умовах, зручним при навігації клавіатурою, безпечним для людей з епілепсією, та шанобливим до системних налаштувань користувача.

Loading diagram...
mindmap
  root((CSS a11y))
    Фокус
      :focus-visible
      Кастомний outline
    Колір
      Контраст WCAG
      forced-colors
      color-contrast()
    Рух
      prefers-reduced-motion
      Безпечні анімації
    Розміри
      rem замість px
      Touch targets 44px
      Масштабування
    Приховування
      sr-only техніка
      aria-hidden
      visibility vs display
    Друк
      @media print
      page-break

Фокус: найчастіша помилка у CSS

Чому outline: none — злочин

Перегляньте будь-який CSS-проєкт 2015–2020 років і ви майже напевно знайдете цей рядок:

/* ⛔ Так НЕ робити — ламає навігацію клавіатурою */
* {
  outline: none;
}

Або ще гірше:

/* ⛔ Так теж НЕ робити */
:focus {
  outline: none;
}

Цей код прибирає єдиний візуальний індикатор, що показує людям, які навігують клавіатурою (Tab, Shift+Tab, Enter), де зараз знаходиться фокус. Без нього навігація клавіатурою стає неможливою.

Хто використовує навігацію клавіатурою:

  • Люди з моторними порушеннями (не можуть використовувати мишу)
  • Сліпі та слабозорі (скринридери + клавіатура)
  • Досвідчені розробники та power-users (значно швидше)
  • Будь-хто з тимчасовою травмою руки

:focus-visible — правильне рішення

:focus-visible — псевдоклас, що активується лише тоді, коли браузер вважає, що візуальний індикатор фокусу необхідний. Зазвичай це:

  • Навігація клавіатурою (Tab)
  • Будь-яка взаємодія, якщо UserAgent вирішив показати індикатор

При кліку мишею :focus-visible зазвичай не активується — і це правильно. Outline при кліку мишею — лише візуальний шум.

🔒localhost:3000

Глобальний скид outline — правильний спосіб

/* ✅ Правильний скид: прибираємо дефолт, зберігаємо власний */
:focus {
  outline: none;
}

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

/* Або для power-users: скинути дефолт та додати власний на рівні компонентів */
:focus:not(:focus-visible) {
  outline: none;
}

Контрастність кольорів

WCAG вимоги

WCAG (Web Content Accessibility Guidelines) — міжнародний стандарт доступності. Він визначає мінімальний контраст між кольором тексту та фону:

РівеньТекст < 18ptТекст ≥ 18pt (великий)
AA (мінімум)4.5:13:1
AAA (ідеал)7:14.5:1
🔒localhost:3000

forced-colors та @media (prefers-contrast)

/* Стилі для режиму підвищеного контрасту (Windows High Contrast) */
@media (forced-colors: active) {
  .button {
    /* forced-colors змінює кольори автоматично, але border залишається видимим */
    border: 2px solid ButtonText;
    forced-color-adjust: none; /* вимкнути авто-зміну для окремих елементів */
  }

  /* Приховані елементи через opacity — можуть бути видимі у forced-colors */
  .decorative {
    forced-color-adjust: none;
    opacity: 0;
  }
}

/* Підвищений контраст на прохання користувача */
@media (prefers-contrast: more) {
  :root {
    --text: #000000;
    --bg: #ffffff;
    --border: 2px solid currentColor;
  }
}

prefers-reduced-motion — безпечні анімації

Деякі людини страждають на вестибулярні розлади або епілепсію — для них мерехтливі анімації можуть бути фізично небезпечними. Операційні системи надають налаштування «Зменшити рух» (Reduce Motion), і CSS дозволяє його зчитувати.

🔒localhost:3000

Правильний паттерн для анімацій:

/* Підхід 1: анімація за замовчуванням, вимикаємо для reduce */
.card {
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }
}

/* Підхід 2: анімація лише при no-preference (рекомендований) */
@media (prefers-reduced-motion: no-preference) {
  .hero {
    animation: slide-in 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }

  .card:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
  }
}

Техніка .sr-only — видимо для скринридера, невидимо візуально

Іноді потрібно надати контекст скринридеру, не показуючи текст візуально. Класичний приклад — кнопка «X» (закрити) без текстового пояснення:

<button class="close-btn">
  <svg>...</svg>
  <span class="sr-only">Закрити діалог</span>
</button>
/* .sr-only — «visually hidden» технніка */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* Варіант з можливістю показу при фокусі (for skip links) */
.sr-only-focusable:not(:focus) {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
🔒localhost:3000

Розміри та масштабування

Чому px для font-size на :root — помилка

Браузери дозволяють користувачам встановлювати базовий розмір шрифту у налаштуваннях (за замовчуванням 16px). Люди з вадами зору встановлюють 20px або 24px. Якщо ви задаєте font-size: 16px на :root — ви перевизначаєте їх налаштування.

🔒localhost:3000

@media print — стилі для друку

🔒localhost:3000

Чеклист доступності CSS

🎯 Фокус

  • Ніколи outline: none без заміни
  • Використовуйте :focus-visible замість :focus
  • Skip link на початку сторінки (Tab → перехід до контенту)

🎨 Кольори

  • Текст ≥ 4.5:1 контраст (AA) або ≥ 7:1 (AAA)
  • Не покладайтесь лише на колір для передачі інформації
  • Тестуйте у forced-colors: active режимі

⚡ Анімації

  • @media (prefers-reduced-motion: no-preference) для анімацій
  • Без блимання частотою 3+ Гц (ризик епілептичного нападу)
  • scroll-behavior: smooth — лише при no-preference

📏 Розміри

  • font-size на :root — тільки % або нічого
  • Touch targets ≥ 44px × 44px
  • Інтерфейс масштабується до 200% без горизонтального скролу

👁️ Приховування

  • .sr-only для тексту що потрібен скринридерам
  • display: none = невидимий для всіх включно зі скринридером
  • opacity: 0 ≠ приховано від скринридера!

🖨️ Друк

  • @media print — ховаємо nav, aside, ads
  • body { font: 12pt serif; color: #000; }
  • a::after { content: " (" attr(href) ")"; }

Практика

Рівень 1 — Базовий: аудит доступності

Беремо готовий HTML-компонент навігаційного меню. Знайдіть та виправте 4 проблеми доступності:

/* Знайдіть проблеми: */
* { outline: none; }                      /* Проблема 1 */
nav a { color: #aaaaaa; }                 /* Проблема 2: контраст ~2.3:1 */
.menu-icon { width: 20px; height: 20px; } /* Проблема 3: замалий touch target */
.tooltip { opacity: 0; }                  /* Проблема 4: чи видно скринридеру? */

Рівень 2 — Адаптивна система Motion

Реалізуйте систему анімацій з повним урахуванням prefers-reduced-motion:

  • Landing hero: slide-in + fade при завантаженні
  • Cards: scale + shadow при hover
  • Scroll: reveal animations через @keyframes
  • При reduce: всі переходи ≤ 50ms, без scale і translate

Рівень 3 — Повна доступна форма

Побудуйте форму відправки повідомлень з:

  • Floating labels (CSS-only)
  • Правильний :focus-visible на кожному полі
  • :user-valid/:user-invalid з ARIA-feedback через aria-live region
  • Error messages через .sr-only для скринридерів
  • Контрастність ≥ 4.5:1 для всього тексту
  • Touch targets кнопок ≥ 44px
  • @media print стиль що показує текст полів без дизайну форм