Контракт доступності
Мінімум, який зобов'язаний виконувати кожен компонент бібліотеки.
Це не побажання, а перелік того, що має бути в кожному компоненті. Більшість пунктів тут з'явилися тому, що в одному з вихідних проєктів вони були порушені.
Фокус завжди видимий
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
Нижче цього палець промахується. Тому на мобільних усі поля й кнопки
зводяться до h-11/h-12, а md: повертає щільні десктопні розміри.
Там, де елемент мусить лишатися дрібним (перемикач у списку налаштувань),
навколо нього додається невидима зона 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 вказує рівно на один активний опис. Якщо передати і
підказку, і помилку, скрінрідер зачитає інструкцію, яку користувач щойно
порушив, раніше за причину відмови.