Как стать автором
Обновить

Всё, что можно автоматизировать, должно быть автоматизировано. Даже aria-label

Уровень сложностиПростой
Время на прочтение4 мин
Количество просмотров909

Я написала свой ESLint-плагин для доступности. Вот как и зачем.

Я люблю автоматизацию: если что-то можно доверить инструменту, это стоит делать. Особенно то, что повторяется из проекта в проект: aria-label, alt, tabIndex.

Линтер - это как фоновый напарник: один раз настроил - и он работает. Не устает, не отвлекается, не забывает. А в контексте доступности, где многое завязано на деталях, это особенно важно.

Почему мне захотелось написать свой плагин?

На самом деле, всё началось не с бага. Скорее - с исследовательского интереса: могу ли я в одиночку сделать полезный open source-проект, с тестами, публикацией и реальной практической ценностью?

Я изучила существующие решения и увидела, что eslint-plugin-jsx-a11y, несмотря на свою силу, оставляет важные вещи вне поля зрения:

Он хорошо работает с нативными HTML-элементами, но теряется на кастомных компонентах. <button> он проверит, а <MyButton onClick={...}> - проигнорирует.

Он требует alt у <img>, но не различает контентные и декоративные изображения. Так легко забыть добавить role="presentation" и не заметить.

Он не понимает, где важна локализация. Для него aria-label="Close" и aria-label={t('close')} - одинаковы. А на многоязычном сайте это большая разница: скринридер может читать страницу на русском, а кнопку - на английском.

Он почти не проверяет поведенческую доступность. Например, div с role="button" без tabIndex и onKeyDown визуально выглядит интерактивно, но не работает с клавиатурой: ни фокус, ни нажатие Enter/Space не срабатывают.

Такие вещи легко пропустить: они не видны на глаз и часто проходят код-ревью. Мне хотелось создать линтер, который будет ловить именно такие "невидимые" проблемы.

Как устроен мой плагин

Технологии: TypeScript, ESLint RuleTester, Jest.

Каждое правило - это обход абстрактного синтаксического дерева (AST). Он позволяет проанализировать JSX-разметку: какие компоненты используются, какие у них атрибуты и как они связаны.

Структура проекта:

src/rules/ - каждое правило в отдельном файле

tests/ - автотесты на правильные и ошибочные примеры

src/index.ts - единая точка входа для подключения всех правил

 

Я сразу писала как npm-пакет с поддержкой ESLint v9, потому что:

  • с 9-й версии используется новый формат конфигурации (eslint.config.js);

  • плагин перешёл на ES Modules;

  • это избавит пользователей от лишней миграции в будущем.

Описание правил

require-aria-label

Что проверяет:
Находит <button>, <input type="button"> и кастомные компоненты без aria-label, title или текстового содержимого.
Полезно, когда используются иконки (<IconButton icon="close" />) без явного текста.

//❌ Плохо: не озвучивается скринридером
<button><Icon name="close" /></button>

//✅Хорошо:
<button aria-label="Close"><Icon name="close" /></button>

no-missing-aria-labelledby-target

Что проверяет:
Если в aria-labelledby="some-id" указана ссылка, то должен существовать элемент с id="some-id".
Без этого атрибут не работает, а скринридер озвучит "unlabeled".

//❌ Плохо:
<div aria-labelledby="title" />

//✅ Хорошо:
<h2 id="title">Dialog</h2><div aria-labelledby="title" />

interactive-supports-focus-and-keys

Что проверяет:
Если элемент с role="button" — он должен быть фокусируемым (tabIndex ≥ 0) и реагировать на клавиатуру (onKeyDown или onKeyPress).
Это особенно важно для div, span и кастомных интерактивов.

//❌ Плохо:
<div role="button" onClick={...} />
                            
//✅ Хорошо:
<div role="button" tabIndex={0} onClick={...} onKeyDown={...} />

no-hardcoded-accessibility-text

Что проверяет:
Если используется aria-label, alt или title, и внутри строка вроде "Close" — будет предупреждение.
Ожидается вызов i18n-функции (t('close')), чтобы не нарушать локализацию.

// ❌ Плохо:
<button aria-label="Close" />
// ✅ Хорошо:
<button aria-label={t('close')} />

img-requires-alt-or-role

Что проверяет:
Каждое <img> должно иметь или alt="...", или быть помечено как декоративное (role="presentation").
Это базовый принцип доступности, особенно важен для логотипов, иконок, аватаров.

// ❌ Плохо:
<img src="logo.png" />
// ✅ Хорошо:
<img src="logo.png" alt="Company Logo" /><img src="..." role="presentation" />

Кому может быть полезен этот плагин:

  • Командам с кастомной дизайн-системой (<Button>, <Modal>, <Avatar> и т.п.)

  • Проектам с мультиязычным интерфейсом

  • Тем, кто внедряет доступность поэтапно и хочет начать с базовых проверок

  • Разработчикам, которые подключают линтер к CI или используют pre-push хуки

  • Всем, кому важно, чтобы интерфейсы были не только красивыми, но и доступными

Как использовать?

npm install eslint-plugin-a11y-extended --save-dev

И подключить в eslint.config.js:

import plugin from 'eslint-plugin-a11y-extended';

export default [
  {
    plugins: { 'a11y-extended': plugin },
    rules: {
      'a11y-extended/require-aria-label': 'warn',
      // и другие
    },
  },
];

Работа над плагином

Сейчас в плагине 5 правил. Каждое из них закрывает важный, но часто пропускаемый случай: от aria-label и alt, до локализации и поведения интерактивных элементов.

Плагин уже сейчас помогает находить ошибки, которые раньше могли попасть в продакшен незаметно. И в этом его главная задача — помочь, а не заменить разработчика.

Зачем все это?

Я люблю автоматизацию. Если можно один раз настроить инструмент, чтобы он отслеживал важные детали — я за.

Мне хотелось попробовать: можно ли в одиночку сделать что-то полезное? Можно.

И теперь у меня есть плагин, который проверяет именно те вещи, что раньше легко проходили мимо — aria-label, alt, tabIndex, локализацию и поведение.

Если вам это близко — пробуйте. А если захочется предложить идею или новое правило — пишите, буду рада.

GitHub — mariaparfenyuk/eslint-plugin-a11y-extended
npm — eslint-plugin-a11y-extended

Теги:
Хабы:
+7
Комментарии4

Публикации

Работа

Ближайшие события