$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогЧистая вёрстка: доступность (a11y) как обязательная часть интерфейса, а не «потом допилим»

Чистая вёрстка: доступность (a11y) как обязательная часть интерфейса, а не «потом допилим»

$ sudo teach IT
·20 июля 2026 г.·9 мин·73
Чистая вёрстка: доступность (a11y) как обязательная часть интерфейса, а не «потом допилим»

Разберём фокус, aria-атрибуты, семантику, клавиатурную навигацию и практические правила, которые улучшают UX для всех.

Содержание
Почему a11y нельзя откладывать1) Доступность — это структура, а не косметика2) Поведение сложнее, чем кажется3) “Латки” часто ухудшают UXСемантика: основа без ariaИспользуйте нативные элементы управленияНе заменяйте семантику контейнерамиЗаголовки и структура документаARIA: когда нужен и когда вреденГлавная философия: “сначала нативное”Accessible name: что и как объявляетсяaria атрибуты для динамики: aria-expanded, aria-controls, aria-liveОшибки с aria: самые частыеКлавиатурная навигация: от таба до логики поведения1) Порядок табуляции должен быть предсказуемым2) Обязательно есть видимый focus3) Обрабатывайте клавиатурные события, если вы не используете нативные элементы4) Модалки и диалоги: фокус и возвратФормы и ошибки ввода: доступность как часть UXПравильные label и связностьСообщения об ошибках: aria-describedbyНе используйте placeholder как labelПрактические правила чистой верстки с a11y-качествомH1: мыслите семантикой, не контейнерамиH2: не дублируйте семантику и не “переписывайте” нативноеH3: фокус — это UXH4: dynamic state должен быть синхронизирован с ariaH5: тестируйте не только в одном браузереКак внедрять доступность в процесс разработкиАвтоматизированный аудит — не замена, а стартовая точкаМинимальные ручные тесты, которые стоит сделать привычкойДокументируйте паттерны компонентовПрактический пример: доступный выпадающий список

Доступность (a11y, accessibility) — это не набор «магических атрибутов» и не чекбокс для отчёта. Это свойство интерфейса, которое проявляется во всех этапах: от семантики разметки до логики фокуса и корректных реакций на клавиатуру. И самое важное: доступность почти всегда ломается не на стадии “добавить aria”, а на стадии проектных решений — когда UI собирают из div и span, игнорируя смысл элементов, а затем пытаются «залатать» поведение.

В этой статье разберём, как строить чистую вёрстку так, чтобы доступность была не «потом допилим», а фундаментальным качеством. Мы поговорим о семантике, роли и назначении aria-атрибутов, навигации с клавиатуры, типичных ошибках, которые встречаются в реальных интерфейсах, и практических правилах, которые помогут улучшить UX для всех пользователей — включая тех, кто полагается на скринридеры, управляет страницей клавиатурой или испытывает трудности со зрением и вниманием.


Почему a11y нельзя откладывать

Есть устойчивый миф: «сначала сделаем интерфейс, а потом прикрутим доступность». На практике это почти всегда приводит к трём проблемам.

1) Доступность — это структура, а не косметика

Семантика — основа того, как ассистивные технологии интерпретируют интерфейс. Если мы построили кнопку не как кнопку, а как div с обработчиком клика, то это нарушает базовую модель взаимодействия. В таком случае aria-атрибуты будут работать не так, как ожидается, а поведение придётся воспроизводить вручную.

2) Поведение сложнее, чем кажется

Клавиатурная навигация, порядок фокуса, видимость фокуса, корректные состояния (например, aria-expanded), работа модалок/всплывающих подсказок — всё это требует согласованной логики. Чем позже заняться этим, тем больше мест придётся переделывать: верстка, JS-события, стили, тесты.

3) “Латки” часто ухудшают UX

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

Вывод простой: доступность должна быть в дизайне и в техническом исполнении с самого начала. Дальше — конкретика.


Семантика: основа без aria

Начнём с того, что в большинстве случаев aria не нужен вообще. Правильная семантика — это “естественный язык” для браузера и ассистивных технологий.

Используйте нативные элементы управления

Кнопка должна быть кнопкой:

code
<button type="button">Сохранить</button>

Ссылка — ссылкой:

code
<a href="/profile">Профиль</a>

Поле ввода — input, select или textarea, а не их имитация:

code
<label for="email">Email</label>
<input id="email" type="email" name="email" />

Причина практическая: нативные элементы автоматически поддерживают:

  • клавиатурное взаимодействие (Enter/Space, таб-навигацию),
  • корректное объявление экранными дикторами,
  • доступные имена (accessible name) по правилам браузера,
  • базовые состояния и “метки” (например, checked, disabled).

Не заменяйте семантику контейнерами

Типичный анти-паттерн:

code
<div role="button" tabindex="0" onclick="...">Сохранить</div>

Иногда так делают, но почти всегда правильнее пересобрать в нативный button. Если это невозможно (например, сложная разметка), тогда aria и поведение должны быть выстроены аккуратно — но это исключение, а не правило.

Заголовки и структура документа

Скринридеры используют структуру документа: заголовки, списки, разделы. Проблема возникает, когда интерфейс “собирают” из случайных div без h1-h6. Даже если визуально всё выглядит ок, доступные технологии будут читать страницу иначе, и пользователь может потеряться.

Практическое правило: если в макете есть визуальные уровни заголовков — перенесите их в h-теги.


ARIA: когда нужен и когда вреден

ARIA (Accessible Rich Internet Applications) — это механизм, который добавляет описания и роли элементам. Но в идеале он дополняет нативное поведение, а не заменяет его.

Главная философия: “сначала нативное”

Если вы можете получить корректное поведение без aria — не добавляйте aria.

Например, у поля ввода есть label — добавлять aria-label не нужно. А если нужно скрыть декоративный элемент — используйте aria-hidden="true" или role="presentation" (в зависимости от ситуации), но не “на всё подряд”.

Accessible name: что и как объявляется

Скринридер обычно объявляет “имя” элемента. Оно формируется из:

  • текста в <label>,
  • aria-label / aria-labelledby,
  • alt у изображений,
  • иногда из содержимого элемента.

Практический пример для кнопки с иконкой:

code
<button type="button" aria-label="Открыть настройки">
  <svg aria-hidden="true" ...>
    ...
  </svg>
</button>

Здесь aria-label нужен именно потому, что видимый текст отсутствует. Но если текст есть — лучше использовать его напрямую.

aria атрибуты для динамики: aria-expanded, aria-controls, aria-live

ARIA особенно полезен для динамических компонентов:

Складной блок (accordion)

code
<button
  type="button"
  aria-expanded="false"
  aria-controls="panel-1"
  id="accordion-1"
>
  Подробнее
</button>

<div id="panel-1" role="region" aria-labelledby="accordion-1" hidden>
  Контент...
</div>

При раскрытии:

  • меняем aria-expanded="true",
  • убираем hidden,
  • корректно фокусируем элемент/перемещаем фокус по UX-правилам.

Область live для уведомлений

code
<div role="status" aria-live="polite" aria-atomic="true">
  Данные сохранены
</div>

Это позволяет объявить сообщение экранному диктору, не требуя ручного “обновления страницы”.

Ошибки с aria: самые частые

  1. Неправильные роли
    Например, role="button" без полноценного keyboard support. Роль — это обещание поведения. Если обещание не выполняется, UI становится проблемным.

  2. Дублирование имени
    Если у кнопки есть текст, но вы добавили aria-label, экранный диктор может объявлять два разных имени или путать пользователя.

  3. aria-live “на всё”
    Слишком активные live-области создают “информационный шум”. Сообщайте только то, что важно.

  4. Отсутствие синхронизации состояния
    Если aria-expanded не меняется вместе с видимым состоянием — пользователь с ассистивными технологиями будет видеть другой “мир”, чем вы показываете визуально.


Клавиатурная навигация: от таба до логики поведения

Клавиатура — базовый тест доступности. Если интерфейс нельзя использовать без мыши, это не “почти доступно”, а скорее всего недоступно для части пользователей.

1) Порядок табуляции должен быть предсказуемым

Естественный порядок задаётся DOM. Если вы меняете порядок визуально (например, с помощью flex/grid reorder) — следите, чтобы таб-навигация не превращалась в случайную прогулку.

Практическое правило: используйте CSS перестановки (order) реже или очень осознанно. Если это необходимо — выстраивайте порядок focus вручную, но это усложняет поддержку и тестирование.

2) Обязательно есть видимый focus

Частая ошибка — “обнулить” стили фокуса в CSS. В итоге пользователь клавиатуры не понимает, где он находится.

Не надо “красить красиво любой ценой”, но focus outline должен быть видимым.

Пример безопасного поведения:

code
:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
}

focus-visible помогает избежать случайного outlines при клике мышью, но сохраняет видимость фокуса для клавиатуры.

3) Обрабатывайте клавиатурные события, если вы не используете нативные элементы

Когда вы всё же создаёте кастомный контрол (например, список вариантов, который визуально похож на кнопки), нужно обеспечить:

  • доступ к элементу (tabindex),
  • реакцию на Enter/Space (если это “кнопочное” поведение),
  • корректную работу со role.

Однако лучше путь — избегать кастомных кнопок поверх div и использовать button, a, select.

Если же кастом неизбежен, то минимальный “честный” каркас:

code
<div
  role="button"
  tabindex="0"
  id="custom-btn"
>
  Кастомная кнопка
</div>

<script>
  const el = document.getElementById('custom-btn');
  el.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      el.click();
    }
  });
</script>

Но всё равно это хуже нативного button: нативные элементы дадут больше “из коробки”.

4) Модалки и диалоги: фокус и возврат

Модальное окно должно:

  • получать фокус при открытии,
  • “держать” фокус внутри (focus trap),
  • возвращать фокус на элемент-переключатель при закрытии,
  • корректно закрываться Escape (обычно ожидаемо пользователями).

Здесь не приводим “универсальную библиотеку в вакууме”, но общая схема такая:

  • Сохраняете ранее сфокусированный элемент.
  • При открытии модалки переносите фокус на первый интерактивный элемент.
  • Перехватываете Tab внутри.
  • При закрытии возвращаете фокус назад.

Если вы делаете это позже, часто рушится поведение: кнопки закрытия остаются недостижимыми с клавиатуры или фокус “улетает” под оверлей.


Формы и ошибки ввода: доступность как часть UX

Формы — самый частый участок, где a11y реально влияет на конверсию и качество. Здесь важны не только роли, но и коммуникация ошибок.

Правильные label и связность

code
<label for="password">Пароль</label>
<input id="password" type="password" name="password" />

Если label не может быть текстовым рядом — используйте aria-label или aria-labelledby, но лучше дизайн подстроить под label (это проще для поддержки и читаемости).

Сообщения об ошибках: aria-describedby

Когда есть ошибка, желательно:

  • показать текст ошибки визуально,
  • объявить ошибку для ассистивных технологий,
  • связать ошибку с полем.

Пример:

code
<label for="email">Email</label>
<input id="email" type="email" name="email" aria-describedby="email-error" />

<div id="email-error" role="alert" hidden>
  Похоже, email введён с ошибкой
</div>

При валидации:

  • снимаем hidden,
  • убеждаемся, что текст ошибки появляется и связан через aria-describedby.

role="alert" даёт приоритетное объявление. Иногда достаточно aria-live="polite", но для критичных ошибок чаще используют именно alert.

Не используйте placeholder как label

Placeholder может исчезать при вводе, он не считается полноценной меткой. aria-label не всегда спасает, потому что placeholder зачастую используется как “единственный текст” для идентификации поля — а это плохо для многих сценариев.


Практические правила чистой верстки с a11y-качеством

Соберём в “рабочий чек-лист”, который удобно применять в разработке.

H1: мыслите семантикой, не контейнерами

  • Заголовки — h1-h6.
  • Списки — ul/ol/li.
  • Ссылки — a.
  • Кнопки — button.
  • Форма — form, label, input и т.д.

H2: не дублируйте семантику и не “переписывайте” нативное

  • Если элемент уже делает нужное поведение — не добавляйте aria.
  • Если добавляете role/aria — проверьте соответствие поведения.

H3: фокус — это UX

  • Не отключайте outline.
  • Делайте :focus-visible.
  • Проверяйте весь сценарий “только клавиатура”.

H4: dynamic state должен быть синхронизирован с aria

  • aria-expanded ↔ фактическое открытие/закрытие.
  • aria-controls ↔ реально существующий id.
  • Ошибки/успехи ↔ live/alert сообщения.

H5: тестируйте не только в одном браузере

ARIA и фокус — зона реальной совместимости, особенно в кастомных компонентах. Минимальный набор проверок:

  • Chrome + NVDA / экранный диктор,
  • Firefox + NVDA,
  • Safari (хотя бы базовые сценарии),
  • мобильные сценарии (где применимо).

Как внедрять доступность в процесс разработки

Чтобы “a11y потом” не превращалось в “a11y никогда”, нужна дисциплина. Это не только про код, но и про то, как вы проектируете.

Автоматизированный аудит — не замена, а стартовая точка

Инструменты (линтеры, проверки, расширения) хорошо ловят:

  • отсутствующие атрибуты,
  • противоречия между ролью и типом элемента,
  • пустые href,
  • некоторые проблемы с контрастом (если инструмент поддерживает).

Но они не проверяют:

  • логический порядок фокуса,
  • корректные клавиатурные сценарии,
  • смысл доступных имён в конкретном контексте.

Минимальные ручные тесты, которые стоит сделать привычкой

  1. Отключите мышь и пройдите страницу табом.
  2. Откройте каждый интерактивный компонент клавиатурой.
  3. Проверьте Esc/Enter/Space там, где ожидается.
  4. Откройте скринридер и убедитесь, что “имя” и состояние объявляются осмысленно.
  5. Проверьте формы: ошибки объявляются и привязаны к полям.

Документируйте паттерны компонентов

Самая экономная практика — сформировать библиотеку “как мы делаем модалки”, “как мы делаем тулы/меню”, “как мы делаем select-like контролы”. Тогда доступность не зависит от героизма конкретного разработчика.


Практический пример: доступный выпадающий список

Рассмотрим простой компонент: выпадающий список, который открывается кнопкой и показывает набор вариантов. Ошибка многих реализаций — “неправильная роль” и некорректный focus.

Нативный вариант часто проще, но допустим кастом:

code
<div>
  <button
    type="button"
    id="dd-btn"
    aria-haspopup="listbox"
    aria-expanded="false"
    aria-controls="dd-list"
  >
    Выберите язык
  </button>

  <ul
    id="dd-list"
    role="listbox"
    aria-labelledby="dd-btn"
    tabindex="-1"
    hidden
  >
    <li role="option" aria-selected="true" tabindex="-1">Русский</li>
    <li role="option" aria-selected="false" tabindex="-1">English</li>
    <li role="option" aria-selected="false" tabindex="-1">Deutsch</li>
  </ul>
</div>

<script>
  const btn = document.getElementById('dd-btn');
  const list = document.getElementById('dd-list');
  const options = Array.from(list.querySelectorAll('[role="option"]'));

  function open() {
    btn.setAttribute('aria-expanded', 'true');
    list.hidden = false;
    list.focus(); // фокус уходит в компонент
  }

  function close() {
    btn.setAttribute('aria-expanded', 'false');
    list.hidden = true;
    btn.focus(); // возврат фокуса
  }

  btn.addEventListener('click', () => {
    const isOpen = btn.getAttribute('aria-expanded') === 'true';
    isOpen ? close() : open();
  });

  btn.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      open();
    }
  });

  list.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      e.preventDefault();
      close();
      return;
    }

    const activeIndex = options.findIndex(opt => opt.getAttribute('aria-selected') === 'true');
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      const next = Math.min(activeIndex + 1, options.length - 1);
      options[activeIndex].setAttribute('aria-selected', 'false');
      options[next].setAttribute('aria-selected', 'true');
      options[next].focus();
    }

    if (e.key === 'ArrowUp') {
      e.preventDefault();
      const prev = Math.max(activeIndex - 1, 0);
      options[activeIndex].setAttribute('

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

Автор

$ sudo teach IT

HTML/CSS

Курс по теме

HTML/CSS

Noldan HTML va CSS'da o'z saytlaringizgacha. Sehr yo'q — faqat teglar, stillar va birinchi loyihangiz. Nega hammasi buzilishini taxmin qilishni bas qiling — chiroyli va hamma joyda ishlaydigan qilib versta qilishni o'rganing.

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

Перейти к курсу

Другие статьи

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке

Разберём понятный маршрут для новичка: как выбрать первый язык, что учить в первую очередь, какие мини-проекты делать и как не забросить занятия. Дам бесплатные ориентиры и план, чтобы знания складывались в портфолио.

21 сентября 2026 г.
1950
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
100
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1950
Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами
такое

Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами

Разберём, что такое базы данных и SQL без сложной теории: как выбирать данные, фильтровать их и собирать таблицы в отчёт. В конце — короткий план, как потренироваться и собрать первые запросы для портфолио.

28 октября 2026 г.
220
Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck
деплой

Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck

Разберём практический чек-лист подготовки контейнера к продакшену: как собрать воспроизводимый образ, как правильно прокидывать конфиги/секреты через окружение и как настроить healthcheck, чтобы деплой не “успевал” раньше готовности сервиса.

18 сентября 2026 г.
1930
Что такое Git и зачем он нужен новичку: как сохранять код и не бояться правок
такое

Что такое Git и зачем он нужен новичку: как сохранять код и не бояться правок

Разберём простыми словами, что делает Git и чем он отличается от “сохранить файл”. Дойдём до первых действий: создать репозиторий, сделать коммит и посмотреть историю изменений.

26 октября 2026 г.
250

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему a11y нельзя откладывать1) Доступность — это структура, а не косметика2) Поведение сложнее, чем кажется3) “Латки” часто ухудшают UXСемантика: основа без ariaИспользуйте нативные элементы управленияНе заменяйте семантику контейнерамиЗаголовки и структура документаARIA: когда нужен и когда вреденГлавная философия: “сначала нативное”Accessible name: что и как объявляетсяaria атрибуты для динамики: aria-expanded, aria-controls, aria-liveОшибки с aria: самые частыеКлавиатурная навигация: от таба до логики поведения1) Порядок табуляции должен быть предсказуемым2) Обязательно есть видимый focus3) Обрабатывайте клавиатурные события, если вы не используете нативные элементы4) Модалки и диалоги: фокус и возвратФормы и ошибки ввода: доступность как часть UXПравильные label и связностьСообщения об ошибках: aria-describedbyНе используйте placeholder как labelПрактические правила чистой верстки с a11y-качествомH1: мыслите семантикой, не контейнерамиH2: не дублируйте семантику и не “переписывайте” нативноеH3: фокус — это UXH4: dynamic state должен быть синхронизирован с ariaH5: тестируйте не только в одном браузереКак внедрять доступность в процесс разработкиАвтоматизированный аудит — не замена, а стартовая точкаМинимальные ручные тесты, которые стоит сделать привычкойДокументируйте паттерны компонентовПрактический пример: доступный выпадающий список