Skip to content

Веб-доступність у 2026: скринридери, WCAG 2.2 і практики, що реально працюють

Доступність - це не чекліст, який запускають наприкінці спринту. Це набір структурних рішень - семантичний HTML, управління фокусом, ARIA-контракти - які або закладаються під час розробки, або коштують утричі дорожче при рефакторингу.

Веб-доступність: скринридер, клавіатурна навігація, діаграма WCAG 2.2 та ARIA roles
Автор:Опубліковано:Оновлено:Час читання:22 хв читання

Senior Frontend Architect, 10+ років досвіду побудови production-проєктів на Next.js. Contentful Certified Professional (2024). Спеціалізація: React Server Components, headless eCommerce, інженерія Core Web Vitals.

Торік я три дні аудитував чекаут з оцінкою дизайну 4,8 з 5 у Figma. Візуально все було щільно й акуратно. Тільки от у поле промокоду не можна було потрапити табом: це був <div> з обробником кліку, без tabindex і без ролі. Користувачі скрінрідерів чули «без мітки» на кожному полі, бо дизайнер використав плейсхолдер замість лейбла, а розробник скопіював цей патерн далі. Повідомлення про помилки червоніли, але програмно з полями зв'язані не були. Кнопка «Оформити замовлення» жила всередині кастомного компонента, який перехоплював Enter, але не Space. Жоден із цих багів не був видний тому, хто користується мишею.

У провалів доступності завжди одна й та сама природа: вони невидимі для більшості команди й повністю блокують конкретну групу користувачів. У цьому пості я розбираю, як скрінрідери насправді парсять DOM, чого вимагає WCAG 2.2 у термінах конкретної реалізації, які патерни клавіатурної навігації ламаються найчастіше і як вибудувати тестування, що ловить це раніше за користувачів. Пишу з позиції фронтенд-інженера: на специфікацію посилаюся там, де це важливо, але фокус на тому, що реально ламається в продуктах і як це лагодити.

Частина I - Хто використовує асистивні технології й чому це важливіше за просте дотримання вимог

Рамка «доступність - це про інвалідність» пропускає більшу частину тих, кого вона стосується. За оцінкою ВООЗ, 1,3 мільярда людей, тобто 16% населення планети, живуть з тією чи іншою формою інвалідності. Але спектр значно ширший за постійні стани: людина зі зламаною рукою ходить сайтом з клавіатури. Людина на яскравому сонці потребує високого контрасту. Людина після струсу не дає ради щільному й швидко змінному вмісту. Людина в шумному місці вмикає субтитри. Інклюзивний дизайн, який дає раду цим випадкам, служить не лише людям з інвалідністю: він дає кращий інтерфейс усім, хто опинився в неідеальних умовах.

  • Користувачі скрінрідерів: У США близько 7,6 мільйона людей із порушеннями зору (Бюро перепису США). У світі ВООЗ нараховує 285 мільйонів. Частки скрінрідерів: JAWS (38%), NVDA (31%), VoiceOver на iOS і macOS (18%), TalkBack на Android (7%). Це не крайні випадки: NVDA безкоштовний, VoiceOver іде з кожним пристроєм Apple, TalkBack вбудований в Android. Ваші користувачі вже ними користуються.
  • Лише клавіатура: Рухові порушення зачіпають приблизно 2 мільйони американців, які використовують альтернативне введення - перемикачі, пристрої sip-and-puff, айтрекінг. Усе це на рівні операційної системи емулює клавіатуру. Якщо клавіатурна навігація зламана, для цієї групи недоступний увесь продукт.
  • Когнітивні особливості й труднощі навчання: Дислексія зачіпає 15–20% населення, СДУГ - 8–10% дорослих. Обом групам допомагають ясна типографська ієрархія, передбачувана навігація, видимий фокус і можливість зупинити або контролювати залежний від часу вміст. Ті самі покращення знижують когнітивне навантаження і в нейротипових користувачів у стресових ситуаціях.
  • Юридичні ризики: ADA у США, EN 301 549 у ЄС та Equality Act у Британії встановлюють вимоги до цифрової доступності. Кількість позовів за ADA до сайтів перевищила 4600 у 2023 році, плюс 42% рік до року (UsableNet). Цільовий рівень відповідності в більшості юрисдикцій - WCAG 2.1 AA; чинна рекомендація W3C - WCAG 2.2 AA.
  • Вплив на бізнес: Сукупний наявний дохід людей з інвалідністю та їхнього найближчого оточення оцінюється в 13 трильйонів доларів (Accenture). Сайти, які валять базові перевірки, втрачають цей сегмент. І практичніше: кожен провал WCAG - це провал юзабіліті, який зачіпає коло ширше, ніж лише людей з інвалідністю.

Частина II - Як скрінрідери реально парсять DOM

Модель, яка знімає більшість помилок з ARIA: скрінрідер читає не HTML, а дерево доступності. Це паралельна структура, яку браузер виводить із DOM і віддає через платформні API (MSAA та UIA у Windows, NSAccessibility у macOS, AT-SPI у Linux). У кожного вузла цього дерева чотири ключові властивості: роль, ім'я, стан і значення. Працюючи зі скрінрідером, ви ходите цим деревом, а не DOM.

  • Роль: Виводиться з тега (<button> дає роль button, <nav> - navigation) або перевизначається атрибутом role (<div role='button'>). Роль каже скрінрідеру, що це за елемент керування і яких взаємодій від нього чекати. <div> без ролі - це generic-контейнер: у режимі читання скрінрідер озвучить його текст, але в інтерактивному режимі користувач не знайде його гарячими клавішами «B» (наступна кнопка) чи «F» (наступне поле форми).
  • Ім'я: Доступне ім'я - це те, що скрінрідер вимовляє, коли фокус потрапляє на елемент. Джерела за пріоритетом: aria-labelledby (посилання на текст іншого елемента), aria-label (явний рядок), вміст елемента (для кнопок і посилань), пов'язаний <label> (для полів форми), атрибут title (запасний варіант, ненадійний). Плейсхолдер джерелом імені НЕ є: він зникає при введенні й надійно не озвучується. Це найчастіший баг із мітками.
  • Стан: Чи розкрито елемент (aria-expanded), чи позначено (aria-checked), чи невалідний (aria-invalid), заблокований (disabled або aria-disabled), обраний (aria-selected). Стан зобов'язаний оновлюватися програмно при зміні: кастомний акордеон, який візуально розкрився, зобов'язаний перемкнути aria-expanded на тригері. Зміни лише на вигляд скрінрідеру не видно.
  • Режим читання проти інтерактивного: У скрінрідерів два основні режими. У режимі читання (за замовчуванням у NVDA і JAWS при завантаженні) стрілки лінійно ходять деревом доступності й озвучують увесь вміст. В інтерактивному режимі (режим форм) клавіші перехоплює сфокусований елемент, а не скрінрідер. Перемикання автоматичне: фокус на текстовому полі вмикає інтерактивний режим, Escape повертає в читання. Тому керовані з клавіатури елементи зобов'язані мати нативну семантику: <div> з onclick режим форм не увімкне, і взаємодіяти з ним не вийде.
  • Віртуальний курсор проти фокуса: У режимі читання скрінрідер тримає віртуальний курсор окремо від клавіатурного фокуса браузера. Віртуальний курсор може стояти на будь-якому елементі, навіть нефокусовному. Тому не можна за CSS :focus судити про те, що скрінрідер читає просто зараз: це лише позиція клавіатурного фокуса. Живі області ARIA (про них у сьомій частині) - спосіб оголосити зміну вмісту, не змушуючи користувача переходити до конкретного елемента.

Частина III - Клавіатурна навігація: патерни, що ламаються найчастіше

У клавіатурної навігації є чітка специфікація: ARIA Authoring Practices Guide (APG) описує очікувану поведінку клавіш для кожного типу віджета. Провали, які я бачу в продакшені, майже завжди йдуть від кастомних компонентів, де реалізовано частину поведінки, але не все, або від керування фокусом, про яке просто не подумали.

  • Порядок табуляції й порядок DOM: За замовчуванням таб іде за порядком вихідників, а не за візуальною розкладкою. CSS grid і flexbox дозволяють переставляти елементи візуально, не чіпаючи DOM, і тоді порядок читання розходиться з порядком табуляції. Правило: порядок у вихіднику має збігатися із задуманим порядком читання. Якщо візуальної перестановки не уникнути, можна застосувати tabindex, але ручне керування ним у складній розкладці крихке.
  • Пастка фокуса в модалках: Коли модалка відкрита, фокус зобов'язаний бути замкнений усередині неї. Tab має ходити лише фокусовними елементами модалки, а з останнього переходити на перший, а не тікати на сторінку позаду. При закритті фокус зобов'язаний повернутися на елемент, який її відкрив. Для цього треба: перехоплювати Tab і Shift+Tab на keydown, збирати всіх фокусовних нащадків (button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])) і загортати перехід по колу. Бібліотеки на кшталт focus-trap роблять це правильно; саме в саморобних реалізаціях і заводяться баги.
  • Неправильне використання tabindex: tabindex='0' робить елемент фокусовним у порядку DOM - для кастомних інтерактивних елементів, не фокусовних нативно. tabindex='-1' робить елемент фокусовним програмно через .focus(), але прибирає з порядку табуляції - для елементів, які отримують фокус кодом, наприклад контейнерів модалок. Значення tabindex більші за нуль - легасі-патерн, який робить порядок непередбачуваним; використовувати його не слід ніколи.
  • Клавіші для кастомних віджетів: APG задає очікувані патерни. Кастомний випадний список: Enter або Space відкриває меню, стрілки ходять опціями, Enter обирає, Escape закриває і повертає фокус на тригер. Кастомні таби: стрілки перемикають вкладки всередині списку (патерн roving tabindex), Tab веде фокус у панель. Акордеон: Enter або Space перемикає. Реалізувати половину гірше, ніж не реалізовувати взагалі: випадний список, який відкривається по Enter, але потребує миші для вибору опції, - це клавіатурна пастка.
  • Посилання пропуску навігації: Першим фокусовним елементом кожної сторінки має бути посилання «Перейти до основного вмісту», яке перестрибує навігацію. Без нього клавіатурний користувач на кожному завантаженні проходить табом усі пункти меню. Зазвичай посилання ховають візуально й показують по фокусу: <a href='#main-content' class='sr-only focus:not-sr-only'>Перейти до основного вмісту</a> плюс <main id='main-content' tabindex='-1'>. Атрибут tabindex='-1' на <main> потрібен, щоб елемент міг прийняти програмний фокус, не з'являючись у порядку табуляції.
  • Видимість фокуса: Критерій WCAG 2.2 SC 2.4.11 (Focus Not Obscured, рівень AA) вимагає, щоб елемент, який отримав фокус, не був цілком схований липкою шапкою, банером про куки чи фіксованим оверлеєм. Використовуйте CSS scroll-margin-top, щоб урахувати висоту липкої шапки. Селектор :focus-visible націлений саме на клавіатурний фокус, а не на клік мишею: з ним можна дати чіткий індикатор, не заважаючи користувачам миші. Штатне кільце фокуса браузера лишається правильною відповіддю з часів Chrome 86 і Safari 15.4; старий прийом outline: none - прямий провал WCAG.

Частина IV - WCAG 2.2: критерії, що реально зустрічаються в аудитах

У WCAG 2.2 сімдесят вісім критеріїв успіху на трьох рівнях відповідності (A, AA, AAA). Рівень AA - юридичний стандарт у більшості юрисдикцій і практична ціль для більшості продуктів. Розбирати всі сімдесят вісім немає сенсу; нижче ті, на які припадає більша частина провалів у продакшен-аудитах, разом із деталями реалізації, які справді важливі.

  • SC 1.1.1 Нетекстовий вміст (рівень A): У кожного зображення має бути текстова альтернатива. Для інформативних alt описує зміст і функцію. Для декоративних - alt='', тобто порожній рядок, а не відсутній атрибут, щоб скрінрідер їх пропустив. Для функціональних (іконки-посилання, іконкові кнопки) alt або aria-label описує дію, а не картинку. Типовий провал: іконкова кнопка без тексту і без aria-label - скрінрідер оголошує «кнопка» без імені, користуватися нічим. Для SVG-іконки всередині кнопки: aria-hidden='true' на іконці плюс видимий або візуально схований текст, або aria-label на кнопці.
  • SC 1.4.3 Контраст (рівень AA): Звичайний текст потребує контрасту 4,5:1 до тла. Великий (від 18pt або 14pt жирним) - 3:1. Елементи інтерфейсу - межі полів, контури кнопок - 3:1 до сусідніх кольорів. Часті провали: світло-сірий плейсхолдер, білий текст на світлому брендовому кольорі, заблоковані елементи з недостатнім контрастом (WCAG 2.2 виводить справді заблоковані елементи з-під вимоги, але лише якщо вони дійсно disabled, а не просто так стилізовані).
  • SC 1.4.4 Масштабування тексту (рівень AA): Текст має читатися при збільшенні до 200% без втрати вмісту й функціональності. Ламається, коли розміри шрифту задані в px замість rem (зум браузера змінює вьюпорт, а не пікселі), коли в контейнерів фіксована висота і текст обрізається, або коли текст лежить у SVG, який не масштабується.
  • SC 2.1.1 Клавіатура (рівень A): Уся функціональність має працювати з клавіатури. Як ламається: кастомні інтерактивні компоненти з обробниками лише під мишу. Кожному обробнику click на ненативному елементі потрібен парний keydown на Enter і часто на Space. Нативні <button> і <a> роблять це самі - це й є головний аргумент на користь нативних елементів проти div-ів з ARIA.
  • SC 2.4.3 Порядок фокуса (рівень A): Фокус має рухатися в порядку, який зберігає сенс і працездатність. Порушується, коли модалки додаються в <body> без керування фокусом, коли динамічний вміст вставляється перед поточною позицією фокуса без оголошення або коли значення tabindex створюють нелогічну послідовність.
  • SC 2.4.7 Видимий фокус (AA) і SC 2.4.11 Фокус не перекрито (AA, новий у 2.2): Фокус зобов'язаний бути видимим (не прибирайте outline), а сфокусований елемент не має цілком ховатися перекривним вмістом на кшталт липкої шапки. SC 2.4.12 (рівень AAA) іде далі: у сфокусованого елемента не має бути перекрито нічого.
  • SC 3.3.1 Визначення помилки (A) і 3.3.2 Мітки (A): Коли виявлено помилку введення, елемент з помилкою має бути визначений, а сама помилка описана текстом - не лише кольором і не лише іконкою. Повідомлення зобов'язане бути програмно пов'язане з полем через aria-describedby (або aria-errormessage разом з aria-invalid='true'). Мітка має називати призначення поля. Плейсхолдер провалює SC 3.3.2, бо зникає і в більшості браузерів не проходить за контрастом.
  • SC 4.1.2 Ім'я, роль, значення (рівень A): У всіх компонентів інтерфейсу ім'я, роль і поточний стан мають визначатися асистивними технологіями. Це загальний критерій для кастомних віджетів: кожен інтерактивний компонент зобов'язаний віддавати свою роль (нативною семантикою або атрибутом role), доступне ім'я і поточний стан (aria-expanded, aria-selected, aria-checked тощо).

Частина V - Типові production-помилки й патерн виправлення

Це провали, які спливають майже в кожному аудиті доступності, що я робив на продакшен-фронтендах інтернет-магазинів і SaaS. У кожного є стійкий патерн виправлення.

  • Кнопки з <div> і <span>: <div onClick={handler}>Click me</div> - ані ролі, ані доступу з клавіатури, ані фокуса. Лікування: <button type='button'> для всіх інтерактивних елементів, крім навігаційних посилань. Якщо змінити елемент не можна, додайте role='button' tabindex='0' onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handler(e); }}. Але нативна <button> завжди краща: вона робить усе це сама, працює з надсиланням форми й має нативний стан disabled.
  • Іконкові кнопки без доступного імені: <button><Icon /></button> оголошується як «кнопка» без мітки. Лікування: aria-label='Закрити діалог' на кнопці або візуально схований текст <span class='sr-only'>Закрити діалог</span>. На SVG-іконці поставте aria-hidden='true', щоб її пропустили. Атрибут title основною міткою бути не повинен: він озвучується непослідовно і не з'являється на тач-пристроях.
  • Поля без постійних міток: Плейсхолдер замість мітки. Лікування: завжди <label for='input-id'> або aria-label. Пов'язані поля групуйте <fieldset> і <legend> - радіогрупи, групи чекбоксів. Помилки зв'язуйте явно: <input aria-describedby='email-error' aria-invalid='true' /> <p id='email-error'>Введіть коректну адресу пошти</p>. Атрибут aria-invalid змушує скрінрідер оголосити «невірне введення» при фокусі на полі.
  • Інформація лише кольором: Помилки показані лише червоним, обов'язкові поля позначені лише червоною зірочкою, статусні бейджі різняться лише кольором. Лікування: доповніть колір текстовою міткою, патерном, іконкою з альтернативою або іншою некольоровою ознакою. Червона зірочка зобов'язана мати розшифровку: <span aria-hidden='true'>*</span><span class='sr-only'>(обов'язково)</span> або видима легенда перед формою.
  • Динамічні зміни без оголошення: Підвантажені результати, тости, живий пошук - ніщо з цього не оголошується скрінрідером, якщо не використати живі області ARIA. Лікування: aria-live='polite' на контейнері, який отримує оновлення. Для статусів на кшталт кількості знайденого: <div role='status' aria-live='polite'>{resultCount} результатів</div>. Для термінового - role='alert' (має на увазі aria-live='assertive'). Контейнер зобов'язаний існувати в DOM до оновлення: вставити живу область і наповнити її в тому самому такті може не спрацювати.
  • Нескінченне прокручування без виходу з клавіатури: Частий патерн у магазинах. Потрапивши табом у нескінченний список, клавіатурний користувач застрягає: список росте швидше, ніж він ним іде. Лікування: або пагінація, доступна за своєю природою, або кнопка «Показати ще» в кінці поточної партії, до якої можна дійти й свідомо продовжити, або посилання «Перейти до кінця результатів».
  • Автовідтворення відео без керування: Порушує SC 1.4.2 (керування звуком) і SC 2.1.1. Фонові відео не повинні стартувати зі звуком. Якщо стартують без звуку, має бути можливість поставити на паузу. У відео із закадровим текстом мають бути субтитри (SC 1.2.2).

Частина VI - Тестування: workflow, який знаходить реальні проблеми

Автоматика ловить десь від 30 до 40% провалів WCAG. Звучить скромно, але ганяти її в CI все одно варто: механічні провали - відсутній alt, порушення контрасту, пропущені мітки - вона знаходить із нульовою граничною вартістю. Решта 60% потребують ручної роботи, а саме навігації лише з клавіатури й тестів зі скрінрідером. Ось порядок, яким користуюся я.

  • Автоматика: axe-core у CI: axe-core - рушій під більшістю інструментів доступності. Підключається через @axe-core/playwright або @axe-core/react для перевірки на рівні компонентів. У CI: проганяйте axe кожною сторінкою маршрутизації й валіть збірку на будь-яких порушеннях рівня critical або serious. Приклад на Playwright: const results = await new AxeBuilder({ page }).analyze(); expect(results.violations.filter(v => ['critical','serious'].includes(v.impact))).toHaveLength(0);. Так автоматично ловляться відсутні мітки, контраст, неправильне ARIA і порушення ролей.
  • Інструменти браузера: панель Accessibility у DevTools: У Chrome DevTools є панель Accessibility (Elements → Accessibility), яка показує дерево доступності для будь-якого елемента разом з обчисленими іменем, роллю і станом. Через неї зручно зрозуміти, чому скрінрідер оголошує не те ім'я, яке ви очікували. Панель доступності в Lighthouse дає швидку оцінку від 0 до 100 і пріоритизований список порушень, але приймати цю оцінку за рівень відповідності не можна: 97 балів не означають відповідності WCAG 2.1 AA.
  • Перевірка клавіатурою: без миші: Пройдіть табом усіма інтерактивними елементами сторінки. Кожен має бути досяжним, видимим у фокусі й керованим через Enter і Space. Перевірте модалки: чи йде фокус усередину при відкритті, чи закриває Escape, чи повертається фокус на тригер. Перевірте форми: чи надсилається Enter, чи досяжні помилки валідації. Перевірте кастомні віджети: чи дотримуються вони клавіатурних патернів APG. Це 15–30 хвилин на сторінку і більша частина неавтоматизовних провалів.
  • Скрінрідери: VoiceOver і NVDA: На macOS - VoiceOver (Command+F5) із Safari. Увімкніть, заплющте очі або відверніться і походіть сторінкою лише з клавіатури, слухаючи оголошення. Ключові перевірки: у кожного інтерактивного елемента осмислене ім'я і роль, зміни стану оголошуються (меню відкрито чи закрито, елемент обрано), повідомлення про помилки зачитуються при появі. На Windows - безкоштовний NVDA з Firefox або Chrome. Користуйтеся режимом читання (стрілки) та інтерактивним (таб полями). JAWS поширений у корпоративному середовищі, але для тривалого тестування потрібна ліцензія.
  • Тести з реальними користувачами: Автоматика і ручна перевірка зрячим розробником ловлять механіку. Для когнітивної й перцептивної доступності потрібні сесії з людьми, які справді живуть з асистивними технологіями: вони знаходять те, що інакше не знаходиться взагалі. Навіть одна сесія на квартал із користувачем скрінрідера розкриває системні патерни, яких автоматика не бачить у принципі.

Частина VII - Доступні патерни компонентів

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

  • Модальні діалоги: Обов'язкові атрибути: role='dialog' (або role='alertdialog' для підтверджень), aria-modal='true' (каже скрінрідеру вважати вміст ззовні інертним), aria-labelledby на заголовок діалогу. Обов'язкова поведінка: фокус іде в діалог при відкритті (на перший фокусовний елемент або на сам контейнер), фокус замкнений усередині, Escape закриває, фокус повертається на тригер. Нативний <dialog> робить більшу частину цього сам - використовуйте його, де можна. <dialog open> дає керування фокусом, поведінку Escape і псевдоелемент ::backdrop. Підтримка браузерами на 2026 рік: 96% (caniuse.com).
  • Валідація форми з живими повідомленнями: Патерн: валідувати поле на blur, усе разом - на надсиланні. При помилці: поставити aria-invalid='true' на поле, вставити повідомлення в <p id='field-error'> і зв'язати через aria-describedby='field-error'. Для зведених помилок, коли впало кілька полів: вставте зведення в область aria-live='assertive' на початку форми, переведіть туди фокус і перелічіть помилки посиланнями, що ведуть до відповідних полів. Так користувач скрінрідера чує помилки одразу і може дійти до кожного поля.
  • Живі області ARIA для тостів: <div role='status' aria-live='polite' aria-atomic='true'> оголошує зміни, не перебиваючи те, що скрінрідер читає зараз. role='alert' (еквівалент aria-live='assertive') перебиває негайно - беріть його лише для критичних помилок, не для рутинних сповіщень. Атрибут aria-atomic='true' велить зачитувати весь вміст області при кожному оновленні, а не лише змінену частину. Монтуйте живу область у кореневому лейауті, щоб вона була завжди, і змінюйте її вміст при появі сповіщення.
  • Кастомні випадні списки проти нативного <select>: Нативний <select> доступний усюди, сам обробляє всю клавіатуру, працює на тач-пристроях і на десктопі й не потребує жодного рядка ARIA. Єдина причина писати своє - свобода стилізації; причина поважна, але ціну треба розуміти: ви переоблаштовуєте клавіатурну навігацію (стрілки, пошук за першими літерами, Escape, Enter), керування фокусом і всі стани ARIA. Якщо все ж пишете: беріть патерн combobox з APG, тестуйте у VoiceOver і NVDA і закладайте постійну підтримку в міру розвитку браузерів.
  • Таблиці даних: Використовуйте нативні <table>, <th>, <td>, <caption>. Заголовкам стовпців потрібен scope='col', заголовкам рядків - scope='row'. Для складних таблиць із багаторівневими заголовками зв'язуйте через id і headers. Тег <caption> дає таблиці доступне ім'я. Не збирайте табличні дані на CSS-гридах і div-ах: скрінрідер не зрозуміє їх як таблицю, доки ви не додасте повну ARIA-семантику, а це помітно більше роботи, ніж нативні елементи.
  • Каруселі й слайдери: Найбільш неправильно реалізовний віджет в електронній комерції. Вимоги: кнопка паузи для автопрокручування (SC 2.2.2), кнопки вперед і назад з описовим aria-label («Наступний слайд: товар 3 з 8»), aria-live='polite' на контейнері слайдів для каруселей без автопрокручування, навігація стрілками між слайдами. Чесна відповідь: товарні каруселі провалюють доступність настільки стабільно, що статична сітка товарів виявляється і доступнішою, і часто конверсійнішою.

Частина VIII - Доступність у Next.js App Router

В App Router є особливості доступності, що випливають із його моделі рендерингу. Головна - переходи між маршрутами: клієнтська навігація не спричиняє того скидання фокуса, яке відбувається при звичайному завантаженні сторінки. У класичних багатосторінкових застосунках перехід автоматично веде фокус на початок сторінки. У застосунку на Next.js клік по <Link> оновлює вміст без повного перезавантаження, а отже фокус лишається там, де був, і користувач скрінрідера може не помітити, що сторінка змінилася.

  • Керування фокусом при переходах: App Router керує фокусом при навігації автоматично починаючи з Next.js 14: він переводить фокус на елемент <body>, що змушує скрінрідер оголосити новий заголовок сторінки. Це краще за попередню поведінку, але гірше, ніж переведення фокуса на перший заголовок нової сторінки. Якщо у застосунку постійний лейаут із липкою шапкою, перевірте, що посилання пропуску навігації працює і після клієнтського переходу: елемент <main> з tabindex='-1' має отримувати фокус при її активації.
  • Серверні компоненти й ARIA: Атрибути ARIA у серверних компонентах - це звичайний серверний HTML, скрінрідер розбирає їх із вихідної відповіді так само, як будь-який інший атрибут. Жодних особливостей тут немає. Тонкість виникає з динамічним станом: aria-expanded='false' відмалювати на сервері можна, а от перемкнути його в true за дією користувача здатен лише клієнтський компонент. Чистий патерн - різати по інтерактивній межі: клієнтський <DisclosureButton> тримає стан aria-expanded, а розкривний вміст приходить серверним компонентом через children.
  • @radix-ui/react-* для доступних примітивів: Radix UI дає доступні нестилізовані примітиви під типові віджети: Dialog, DropdownMenu, Select, Tabs, Accordion, AlertDialog, Tooltip. Кожен реалізує повний патерн ARIA APG для свого типу, включно з клавіатурою, керуванням фокусом і станами. Узяти Radix (або аналог: Headless UI, Ark UI, React Aria від Adobe) фундаментом бібліотеки компонентів - означає отримати коректну поведінку ARIA одразу і витрачати сили на стилізацію, а не на реалізацію доступності. Для команд, що будують дизайн-систему, це найвигідніший шлях.
  • Доступність і Core Web Vitals: Зв'язок прямий. Зсув лейауту (CLS), коли вміст несподівано їде, дезорієнтує людей з когнітивними особливостями й катастрофічний для користувачів скрінрідера, у яких при зміні розкладки з'їжджає віртуальний курсор. Довгі задачі, тобто корінь провалів INP, затримують переміщення фокуса й оголошення живих областей, створюючи розрив між дією користувача і реакцією скрінрідера. Оптимізуючи Core Web Vitals заради швидкості, ви покращуєте і доступність з тих самих причин: повільна стрибуча розкладка б'є непропорційно сильно по тих, хто залежить від передбачуваного тайміну взаємодії. Про продуктивність - у гайді з вебпродуктивності.

Висновок

Борг з доступності в більшості продакшен-застосунків росте з одного джерела: інтерактивні компоненти зібрані із загальних елементів, мітки існують лише візуально, зміни стану невидимі дереву доступності, а керування фокусом ніхто не продумував. Окремо ніщо з цього лагодити не складно; складно лагодити це в масштабі заднім числом.

Практичний шлях уперед: додайте axe-core у CI цього тижня - це година налаштування, і провали почнуть ловитися одразу. Внесіть клавіатурну перевірку у визначення готовності для будь-якого нового інтерактивного компонента. Почніть використовувати нативні елементи там, де зараз стоять <div>: <button>, <a>, <input>, <select>, <details> - і половина WCAG 2.2 AA закриється безкоштовно. Для складних віджетів візьміть Radix UI або React Aria і не пишіть ARIA вручну взагалі.

Аудит доступності або реалізація доступного фронтенду - послуга аудиту доступності | технічне SEO і структуровані дані | обговорити проєкт.

Часті питання

Чим WCAG 2.2 відрізняється від WCAG 2.1?

WCAG 2.2 (опублікований у жовтні 2023) додає до 2.1 дев'ять нових критеріїв успіху, переважно про видимість фокуса, когнітивну доступність і автентифікацію. Ключові додавання рівня AA: SC 2.4.11 (Focus Not Obscured - сфокусований елемент не може бути цілком схований липкою шапкою), SC 2.5.7 (Dragging Movements - у перетягування має бути альтернатива одним вказівником), SC 3.2.6 (Consistent Help - механізми допомоги розташовані на одному місці на всіх сторінках) і SC 3.3.8 (Accessible Authentication - для входу не можна вимагати когнітивних тестів на кшталт головоломок чи запам'ятовування). WCAG 2.2 зворотно сумісний: відповідність 2.2 AA означає відповідність 2.1 AA.

Чи перекриває aria-label видимий текст?

Так. aria-label замінює ім'я, обчислене з вмісту елемента. Для <button aria-label='Закрити'>X</button> скрінрідер оголосить «Закрити, кнопка», а не «X, кнопка». Для іконкових кнопок, де видимий вміст нічого не описує, це правильно. Але aria-label на елементі, чий видимий текст каже інше, створює розбіжність, яка збиває з пантелику тих, хто водночас бачить текст і чує скрінрідер. Використовуйте aria-label лише коли видимого тексту, придатного як ім'я, просто немає.

Чи достатньо, щоб сайт працював з одним скрінрідером?

Ні. VoiceOver із Safari, NVDA з Firefox, JAWS з Chrome і TalkBack з Chrome у межових випадках трактують ARIA по-різному. Опитування користувачів скрінрідерів WebAIM (2024) показує, що JAWS і NVDA разом займають 70% десктопного використання. Мінімум - тестувати у NVDA з Firefox і у VoiceOver із Safari. Компоненти Radix UI й React Aria протестовані в усіх основних сполученнях.

Що робити зі сторонніми компонентами, які не проходять перевірку доступності?

Спершу: заведіть issue у бібліотеці й перевірте, чи немає версії, де це вже полагодили. Для критичних порушень обгорніть компонент і додайте атрибути ARIA на рівні обгортки, де це можливо: aria-label на обгортковому <div> не полагодить відсутні мітки кнопок усередині, але зв'язки через aria-describedby додати в місці використання можна. У крайньому разі напишіть компонент самі або знайдіть альтернативну бібліотеку. Ніколи не залишайте /* eslint-disable jsx-a11y */ як постійне рішення: це ховає проблему, а не розв'язує її.

На який автоматичний показник доступності орієнтуватися?

Оцінка доступності в Lighthouse вище 95 - розумний нижній поріг для CI: усе, що нижче, вказує на провали, які автоматика здатна знайти. Але приймати оцінку за відповідність не можна: сайт може набрати 100 у Lighthouse і при цьому провалити кілька критеріїв WCAG 2.2 AA, які перевіряються лише руками - керування фокусом, клавіатурні взаємодії, поведінка живих областей. Оцінка - випереджальний індикатор, а не сертифікат.

Джерела

Схожі статті

Shopify Hydrogen vs Next.js Commerce: яку архітектуру обрати для магазину у 2026

Детальне порівняння двох основних headless-архітектур для Shopify. Модель виконання, шар даних, стратегія кешування, розмір бандлу, бенчмарки продуктивності (LCP/INP/CLS), TCO, ризик вендор-локіну та практичний фреймворк рішень між нативною глибиною Hydrogen та компонованою гнучкістю Next.js Commerce.

eCommerceNext.jsShopify
Читати статтю

Архітектура веб-продуктивності: системний аналіз 12 інженерних принципів (видання 2026)

Стрибок Lighthouse на 25 пунктів - з 72 до 99 на десктопі - рідко складається зі сотні мікроправок; він береться з перенесення двох-трьох ассетів з основного потоку. Глибокий технічний розбір веб-продуктивності 2026 року: фізика мереж, конвеєри зображень, моделі виконання JavaScript, Critical Rendering Path, edge-обчислення, Core Web Vitals та продакшн RUM - з реальним продакшн-кейсом та конкретними прикладами реалізації.

EngineeringArchitectureCore Web Vitals
Читати статтю

React 19: useOptimistic, use(), Server Actions - механізм і архітектура (2026)

Детальний розбір п'яти нових примітивів React 19: useOptimistic, use(), Server Actions, useActionState, useFormStatus. Action-based mutation архітектура, що замінює Redux/Zustand для серверних мутацій.

React 19ArchitectureNext.js
Читати статтю