Контракт доступності
Мінімум, який зобов'язаний виконувати кожен компонент бібліотеки.
Це не побажання, а перелік того, що має бути в кожному компоненті. Більшість пунктів тут з'явилися тому, що в одному з вихідних проєктів вони були порушені.
Фокус завжди видимий
focus:outline-none без заміни — заборонено. Керування з клавіатури стає
сліпим: користувач тисне Tab і не бачить, де він.
Базовий рівень задано глобально в main.css (:focus-visible { outline }).
Компонент може замінити його на focus-visible:ring-*, але не прибрати.
Кільце малюється токеном --ring і має ring-offset — інакше на суцільній
кнопці воно зливається з її ж заливкою.
Семантичний тег обирається за поведінкою
Клікабельний елемент — <button>, навігаційний — <a>. <div @click>
недоступний із клавіатури й не має ролі.
Саме тому UiChip рендериться як <span> або <button> залежно від
clickable, а UiButton перетворюється на NuxtLink, коли задано to.
Недоступна дія — це <button disabled>, а не посилання з
pointer-events-none: таке посилання лишається в порядку обходу Tab і
спрацьовує на Enter.
Стан не передається лише кольором
Дальтонік і скрінрідер не бачать тону. Якщо UiChip показує статус
крапкою — потрібен label. Якщо перемикач увімкнений — потрібен
aria-checked, а не лише зелений колір.
Ім'я мають усі інтерактивні елементи
Кнопка з самою іконкою зобов'язана мати label. Без нього скрінрідер
зачитає «кнопка» — і все.
Мінімальна ціль дотику 44px
Нижче цього палець промахується. Але «44px» — про ЦІЛЬОВУ ЗОНУ, а не про видиму висоту: збільшувати заливку кнопки заради дотику означає втратити ієрархію розмірів і щільність панелей. Тому контракт бібліотеки такий:
- Поля вводу на мобільному фізично 45px (
h-12за кореня 15px) і текст 16px — iOS Safari не зумує сторінку при фокусі;md:повертає щільні десктопні висоти. - Кнопки й сегменти залишають ієрархію (sm 9 / md 10 / lg 12 на
мобільному), а точність дотику тримає невидима зона 45×45 через
pointer-coarse:after:— той самий прийом, що вUiSwitch. - Пункти випадайок і меню — щільні рядки з паддінгом
py-3на дотику.
Там, де елемент мусить лишатися дрібним (перемикач у списку налаштувань),
невидима зона 44×44 через ::after — щільність списку не страждає.
Оверлеї
Панель має role="dialog", aria-modal="true" і назву.
Фокус утримується всередині й повертається на елемент, з якого відкрили.
Решта прямих дітей <body> отримують inert і aria-hidden.
Escape закриває лише верхній оверлей. Деталі — на сторінці Оверлеї та фокус.
Типово фокус отримує сама панель, а не перший інпут: на мобільних автофокус в інпуті одразу піднімає клавіатуру і з'їдає пів екрана.
Анімації вимикаються
prefers-reduced-motion вимкнено глобально в main.css. Але цього мало
там, де тривалість задана в JS: <Transition> з явним :duration тримав
би елемент у DOM повні 300 мс навіть із вимкненими анімаціями. Для цього є
useReducedMotion().
Опис один, а не два
aria-describedby вказує рівно на один активний опис. Якщо передати і
підказку, і помилку, скрінрідер зачитає інструкцію, яку користувач щойно
порушив, раніше за причину відмови.