Svelte
Доступность: a11y-предупреждения компилятора
Разработчик делает кликабельную карточку товара через div с onclick. Визуально работает, мышью открывается. Компилятор Svelte при сборке выдаёт предупреждение: на div повешен обработчик клика, но нет роли и обработки клавиатуры. Предупреждение проигнорировано. Через месяц приходит жалоба: пользователь, который ходит по сайту с клавиатуры, не может открыть ни одну карточку - Tab её не выделяет, Enter не срабатывает. Замена div на button и пара ARIA-атрибутов чинят это, и предупреждение исчезает.
- Компилятор Svelte - один из немногих, кто проверяет доступность прямо при сборке и предупреждает о проблемах до запуска кода
- WCAG и спецификация WAI-ARIA - отраслевые стандарты доступности, на которые опираются предупреждения компилятора
- Государственные и крупные коммерческие сайты обязаны соответствовать требованиям доступности по закону во многих юрисдикциях
- Скринридеры (VoiceOver, NVDA, JAWS) читают страницу по ролям и подписям, а не по визуальному виду - доступная разметка для них критична
- Playwright и axe-core - стандартная связка для проверки a11y в браузере поверх статических предупреждений Svelte
Предварительные знания
- Синтаксис шаблона Svelte: разметка, атрибуты, обработчики событий вида onclick
- Базовое знание семантического HTML: button, a, label, input
- Идея о том, что по странице можно перемещаться не только мышью, но и клавиатурой (Tab, Enter, Esc)
Предупреждения компилятора о доступности
Большинство инструментов проверяют доступность во время выполнения или в отдельном линтере. Компилятор Svelte встроил часть проверок прямо в сборку: разбирая разметку, он замечает типичные ошибки и выдаёт предупреждение в консоль и в редактор. Это сдвигает обнаружение проблемы влево - к моменту написания кода, а не к жалобе пользователя.
| Предупреждение | Что проверяет | Как чинить |
|---|---|---|
| a11y_click_events_have_key_events | На некликабельном элементе клик без клавиатуры | Использовать button или добавить обработку клавиш и роль |
| a11y_missing_attribute | У img нет alt, у a нет href | Добавить alt с описанием, href или нужный атрибут |
| a11y_label_has_associated_control | label не связан с полем | Связать через for/id или вложить input в label |
| a11y_no_noninteractive_tabindex | tabindex на неинтерактивном элементе | Убрать tabindex или сделать элемент интерактивным семантически |
Предупреждения можно подавить директивой комментария, но это инструмент крайнего случая. Подавление уместно, когда разработчик точно понимает, почему правило в этом месте не применимо. Глушить предупреждение, чтобы оно просто исчезло, означает вернуть проблему доступности обратно в продукт.
В чём ценность того, что Svelte проверяет доступность на этапе компиляции?
ARIA: когда семантики HTML не хватает
ARIA (Accessible Rich Internet Applications) - набор атрибутов, которые сообщают вспомогательным технологиям роль, состояние и свойства элемента. Главное правило, прямо из спецификации: не применять ARIA, если есть подходящий нативный элемент. Нативный button уже несёт роль button, фокусируемость и обработку клавиш. Воссоздавать это на div через role и tabindex - значит писать больше кода ради того, что HTML даёт бесплатно.
Здесь ARIA оправдана: паттерна tabs в HTML нет, поэтому роли tablist, tab, tabpanel и состояние aria-selected описывают скринридеру структуру, которую визуально передают только стили. Атрибут aria-controls связывает вкладку с её панелью. Без этого скринридер увидел бы просто набор кнопок без смысла группировки.
Неверная ARIA хуже её отсутствия. role="button" на div без tabindex и обработки клавиш создаёт элемент, который скринридер объявляет кнопкой, но который не фокусируется и не активируется с клавиатуры. Пользователь слышит обещание, которое интерфейс не выполняет. Нативный button не оставляет шанса на такую рассинхронизацию.
- Сначала искать нативный элемент: button, a, input, details, dialog
- Если паттерна в HTML нет (tabs, дерево, комбобокс) - брать ARIA по соответствующему паттерну из WAI-ARIA Authoring Practices
- Атрибут состояния (aria-selected, aria-expanded) держать в синхроне с реактивным состоянием компонента
- Проверять результат скринридером или axe, а не только глазами
Нужна обычная кнопка действия. Какой подход верный?
Управление фокусом в динамических интерфейсах
В статичной странице браузер сам ведёт фокус по элементам. В динамическом интерфейсе этого мало. Когда открывается модальное окно, фокус должен переместиться внутрь него, иначе пользователь с клавиатуры или скринридером остаётся снаружи и не понимает, что произошло. Когда модалка закрывается, фокус возвращается на элемент, который её открыл, чтобы не потерять место на странице.
Перед открытием код запоминает текущий активный элемент через document.activeElement - это и есть триггер. Затем фокус переводится на контейнер диалога; tabindex со значением -1 делает его программно фокусируемым, не вставляя в обычный обход по Tab. При закрытии фокус возвращается на сохранённый триггер. Проверка instanceof HTMLElement - это type guard, который заменяет небезопасный каст.
Нативный элемент dialog с методом showModal() берёт часть этой работы на себя: ловушку фокуса внутри окна, закрытие по Esc и затемнение фона. Если паттерн укладывается в стандартный диалог, dialog предпочтительнее ручной реализации, потому что доступность встроена в платформу.
Что должно происходить с фокусом при закрытии модального окна?
Навигация с клавиатуры
Часть пользователей не использует мышь вообще: из-за моторных особенностей, при работе со скринридером или просто по привычке. Для них интерфейс должен полностью управляться с клавиатуры. Базовые ожидания: Tab перемещает фокус по интерактивным элементам, Enter и Space активируют, Esc закрывает оверлеи, стрелки двигают по группам (вкладки, меню, список радио).
| Клавиша | Ожидаемое действие | Где |
|---|---|---|
| Tab / Shift+Tab | Переход к следующему / предыдущему элементу | Везде по интерактивным элементам |
| Enter | Активация ссылки или кнопки | Кнопки, ссылки, отправка формы |
| Space | Активация кнопки, переключение чекбокса | Кнопки, чекбоксы |
| Escape | Закрытие модалки, меню, подсказки | Оверлеи и всплывающие элементы |
| Стрелки | Перемещение внутри группы | Вкладки, меню, радиогруппа, слайдер |
Видимый индикатор фокуса обязателен. Распространённая ошибка - убрать обводку через outline: none ради эстетики, не дав замены. Пользователь с клавиатуры перестаёт понимать, где находится. Если стандартная обводка не вписывается в дизайн, её заменяют своим стилем :focus-visible, а не убирают совсем.
Нативные элементы снова экономят труд. button и a реагируют на Enter и Space из коробки, отправка формы по Enter работает без обработчиков. Ручная обработка клавиш нужна там, где паттерна нет в HTML: стрелки между вкладками, Esc для закрытия своего попапа. Чем больше опора на семантику, тем меньше ручного кода клавиатуры и меньше шансов на ошибку.
Дизайнер просит убрать обводку фокуса через outline: none, потому что она портит вид. Как поступить?
Связь с другими темами
Доступность пронизывает разметку, стили и тесты. Связи:
- Тестирование — Playwright с axe ловит a11y-проблемы в реальном браузере там, где статический анализ компилятора бессилен
- Стилизация — Видимый индикатор фокуса и достаточный контраст - часть доступности, решаемая через CSS компонента
Итог
- Компилятор Svelte проверяет доступность статически и выдаёт предупреждения при сборке - это редкая возможность среди фреймворков
- Семантический HTML решает большинство проблем бесплатно: button кликается мышью и клавиатурой, получает фокус и роль без единого атрибута
- ARIA нужна, когда семантики HTML не хватает; первое правило ARIA - не применять ARIA, если есть подходящий нативный элемент
- Управление фокусом обязательно в динамических интерфейсах: при открытии модалки фокус переводится внутрь, при закрытии возвращается на триггер
- Навигация с клавиатуры это не опция: Tab для перехода, Enter и Space для активации, Esc для закрытия, видимый индикатор фокуса
Связанные уроки
- sv-37-testing-vitest-playwright — Playwright с axe проверяет доступность в браузере, дополняя статические предупреждения компилятора во время сборки
- sv-40-styling-scoped-css-variables — Видимый фокус и контраст - часть доступности, и реализуются они средствами стилизации компонентов