Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Доступність вебу та атрибути ARIA». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Доступність вебу** (a11y) - це підхід до розробки сайтів, які працюють для людей з інвалідністю: користувачів екранних читачів, клавіатурної навігації. Семантичний HTML покриває більшість випадків автоматично. ARIA заповнює прогалини для динамічного контенту і кастомних компонентів без нативного HTML-аналога. ```html <div role="alert" aria-live="polite">Форму збережено!</div> ``` **Ключове:** ARIA потрібна лише тоді, коли семантичний HTML не має відповідного елемента.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Доступність вебу** (a11y) - це підхід до розробки сайтів, які працюють для людей з порушеннями зору, слуху та руху: для тих, хто користується екранними читачами або навігацією з клавіатури. ARIA-атрибути - це інструмент для випадків, коли семантичного HTML недостатньо. ## Теорія ### TL;DR - ARIA схожа на шрифт Брайля на музейних експонатах: сам експонат (HTML) вже описує себе, але Брайль заповнює прогалини для тих, кому він потрібен - Семантичні HTML-елементи (`<button>`, `<nav>`, `<main>`) автоматично повідомляють екранним читачам, що вони собою являють; ARIA робить те ж саме для звичайних `<div>` і кастомних компонентів - Правило: використовуй ARIA лише тоді, коли в HTML немає відповідного нативного елемента для того, що ти будуєш - Динамічні сповіщення, модальні вікна і слайдери майже завжди потребують ARIA; статичний контент - зазвичай ні ### Швидкий приклад ```html <!-- Нативний HTML: екранний читач скаже "Відправити, кнопка" без жодного зайвого коду --> <button>Відправити</button> <!-- ARIA заповнює прогалину для динамічного контенту, доданого через JS --> <div role="alert" aria-live="polite">Форму відправлено!</div> <!-- Екранний читач озвучить це одразу, навіть коли JS додає елемент у DOM --> <!-- Кастомна модалка: у HTML немає нативного аналога --> <div role="dialog" aria-modal="true" aria-labelledby="modal-title"> <h2 id="modal-title">Підтвердити дію</h2> <button>Закрити</button> </div> ``` Перший приклад не потребує нічого. Два наступних показують, де ARIA справді потрібна. ### Головна різниця Семантичні HTML-елементи відображаються безпосередньо у вузли Accessibility Tree в браузері. `<button>` стає вузлом з `role="button"`, обчисленим ім'ям із текстового вмісту і вбудованою підтримкою клавіатури - без зайвого коду. ARIA-атрибути на зразок `role="button"` або `aria-expanded` записують у те ж дерево вручну, але поведінку не додають. `<div role="button">` без `tabindex="0"` не отримує фокус. Без обробника `keydown` Enter і пробіл нічого не роблять. Нативний HTML робить усе це автоматично. ### Коли використовувати ARIA - Статичні сторінки з формами, посиланнями та навігацією: тільки семантичний HTML - Повідомлення про помилки або статус, які додає JS: `role="alert"` або `aria-live="polite"` на контейнері - Модальні вікна: `role="dialog"` + `aria-modal="true"` + `aria-labelledby` з посиланням на заголовок - Кастомні компоненти без HTML-аналогів (слайдери, деревоподібні списки, вкладки): ARIA-ролі в поєднанні з обробниками клавіатурних подій - Декоративні іконки всередині підписаних кнопок: `aria-hidden="true"` щоб уникнути подвійного зачитування - Пропускай ARIA, якщо нативний семантичний тег вже покриває сценарій ### Як працює Accessibility Tree Браузер парсить HTML і будує Accessibility Tree поряд із DOM. Екранні читачі звертаються до нього через API операційної системи: Microsoft UI Automation на Windows, Apple Accessibility API на macOS і iOS. Елемент `<button>` отримує вузол з роллю, ім'ям і станом, заповненими автоматично. ARIA-атрибути перевизначають або доповнюють ці властивості під час рендерингу - Blink і WebKit вставляють `aria-expanded` або `aria-valuenow` безпосередньо в дерево. Live-регіони (`aria-live`, `role="alert"`) працюють інакше. Коли JS змінює вміст live-регіону, браузер одразу надсилає сповіщення про diff екранному читачу - не чекаючи, поки користувач перейде до цього місця. Саме тому зміна тексту в звичайному `<div>` не озвучується, а та сама зміна всередині `<div aria-live="polite">` - озвучується. ### Типові помилки **`role="button"` на нативній кнопці:** ```html <button role="button">Клікни</button> ``` Шкоди немає, але екранні читачі ігнорують дублювання ролі. Видали атрибут і покладайся на нативну семантику. **`aria-label` поруч із видимим `<label>`:** ```html <label>Логін: <input aria-label="Поле логін" /></label> <!-- Екранний читач скаже: "Поле логін, поле введення" — дві мітки конфліктують --> ``` Дозволь елементу `<label>` виконати свою роботу. Прибери `aria-label`, якщо правильна асоціація вже є. **Забутий `aria-live` при динамічних змінах:** ```jsx setError('Невірний email'); // <div id="error">{error}</div> — для екранного читача це тиша ``` Рішення: додай `role="alert"` або `aria-live="assertive"` на контейнер до того, як у ньому з'явиться текст помилки. **`aria-expanded` без синхронізації в JS:** Атрибут оголошує стан розкриття. Якщо JS ніколи не змінює його з `false` на `true`, читач повідомить користувача, що меню відкрилось - хоча це не так. Синхронізуй атрибут при кожній взаємодії. ### Де зустрічається - React: Material UI tabs використовують `role="tablist"` з `aria-selected` на кожному таб-елементі - Next.js: `aria-current="page"` на активному `<Link>` для навігаційних landmark-ів - Vue: Vuetify data tables додають `aria-sort` до заголовків колонок із сортуванням - Angular CDK: оверлеї застосовують `aria-label` через `ng-attr-aria-label` - Тестування: панель Accessibility в Chrome DevTools показує обчислене дерево; axe-core автоматично знаходить відсутні мітки і неправильні ролі ### Питання на співбесіді **Q:** Яка різниця між `role="alert"` і `aria-live="polite"`? **A:** `role="alert"` автоматично встановлює `aria-live="assertive"` і перериває поточне зачитування. Використовуй для повідомлень про помилки. `aria-live="polite"` ставить оголошення в чергу і озвучує після завершення поточного речення - підходить для ненагальних статусних повідомлень. **Q:** Чим `aria-hidden="true"` відрізняється від `display: none`? **A:** `aria-hidden` прибирає елемент з Accessibility Tree, але залишає його видимим у DOM. Корисно для декоративних іконок і анімацій. `display: none` прибирає елемент і з візуального макету, і з дерева доступності. **Q:** Чому `tabindex="0"` на `<div>` - це проблема? **A:** Він робить елемент фокусованим, але не дає йому інтерактивної ролі, поведінки клавіатури або ARIA-стану. Для клавіатурних користувачів Tab потрапляє в "глухий кут". Додавай `tabindex="0"` тільки для повноцінного кастомного компонента з усіма потрібними обробниками клавіш. **Q:** Поясни WCAG 4.1.2 Name, Role, Value. **A:** Допоміжна технологія має отримати від кожного інтерактивного елемента три речі: ім'я (з текстового вмісту, `aria-label` або `aria-labelledby`), роль (з HTML-тегу або атрибута `role`) і поточний стан або значення (`aria-valuenow`, `aria-checked`, `aria-expanded`). ARIA існує саме для того, щоб забезпечити це там, де HTML не справляється. **Q:** (Senior) Як працює focus trap у модалці на React-порталі, і що може піти не так при SSR-гідрації? **A:** Встановлюй `aria-modal="true"`, щоб екранний читач сприймав діалог як єдину активну область, і додавай focus trap, що перехоплює Tab і Shift+Tab. При розбіжності гідрації портал може не існувати в початковому HTML з сервера, і модалка монтується без фокусу. Рішення: виклик `containerRef.current.focus()` всередині `useEffect` після монтування і атрибут `inert` на фоновому контенті. VoiceOver в Safari має відому проблему з виходом фокусу з порталів - завжди тестуй цю комбінацію окремо. ## Приклади ### Семантичний HTML проти ARIA-ролей ```html <!-- Статична навігація: семантичний HTML, ARIA не потрібна --> <nav> <a href="/" aria-current="page">Головна</a> <a href="/about">Про нас</a> </nav> <!-- Динамічна помилка: потрібен ARIA live-регіон --> <form> <label for="email">Email</label> <input type="email" id="email" aria-describedby="email-error" /> <!-- role="alert" озвучує помилку одразу при появі тексту --> <div id="email-error" role="alert" aria-live="assertive"></div> </form> <!-- Декоративна іконка всередині підписаної кнопки --> <button aria-label="Закрити діалог"> <svg aria-hidden="true" focusable="false">...</svg> </button> ``` У прикладі з навігацією `aria-current="page"` на активному посиланні - це ARIA, але правильний вибір, бо в HTML немає нативного поняття "поточна сторінка". Все інше - звичайний семантичний HTML. ### Доступна модалка в React ```jsx import { useState, useEffect, useRef } from 'react'; function Modal() { const [isOpen, setIsOpen] = useState(false); const modalRef = useRef(null); useEffect(() => { // Переміщуємо фокус у модалку при відкритті if (isOpen) modalRef.current?.focus(); }, [isOpen]); return ( <> <button onClick={() => setIsOpen(true)}>Видалити акаунт</button> {isOpen && ( <div ref={modalRef} role="dialog" aria-modal="true" aria-labelledby="modal-title" tabIndex={-1} style={{ position: 'fixed', background: 'white', padding: 24 }} > <h2 id="modal-title">Ти впевнений?</h2> <p>Цю дію не можна скасувати.</p> <button onClick={() => setIsOpen(false)}>Скасувати</button> <button>Підтвердити</button> </div> )} </> ); } // NVDA оголосить "dialog Ти впевнений?" при відкритті модалки. // aria-modal каже читачу ігнорувати контент поза діалогом. ``` У продакшені я завжди додаю обробник клавіші Escape для закриття і focus trap, що тримає Tab всередині діалогу. Одного ARIA для повноцінної клавіатурної доступності недостатньо. ### Кастомний ARIA-слайдер з підтримкою клавіатури ```html <div role="slider" tabindex="0" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" aria-label="Гучність" style="width: 200px; height: 20px; background: #ddd;" ></div> <script> const slider = document.querySelector('[role="slider"]'); slider.addEventListener('keydown', (e) => { let val = +slider.getAttribute('aria-valuenow'); if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); // зупиняє прокрутку сторінки slider.setAttribute('aria-valuenow', Math.min(val + 10, 100)); } if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); slider.setAttribute('aria-valuenow', Math.max(val - 10, 0)); } }); </script> ``` Екранний читач озвучує нове значення щоразу, коли змінюється `aria-valuenow`. Без `e.preventDefault()` стрілки одночасно прокручують сторінку і рухають слайдер - баг, який легко пропустити при візуальному тестуванні, але одразу помітний у NVDA.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.