$ 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 мин·60
Чистая вёрстка: доступность (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 – для начинающих!

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

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

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

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

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

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

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

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

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

Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

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

17 сентября 2026 г.
1070
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

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

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630
Запуск и поддержка проекта на Ruff в команде: формат “как в CI”, а не “как договорились”
запуск

Запуск и поддержка проекта на Ruff в команде: формат “как в CI”, а не “как договорились”

Как правильно выстроить конфигурацию Ruff (линтинг, форматирование, автозамены) и сделать так, чтобы одинаковые правила гарантировались в локальной сборке и на CI без ручного шаманства.

16 сентября 2026 г.
1050
Что такое переменные, типы и условия в коде: объясню без матана на примерах
такое

Что такое переменные, типы и условия в коде: объясню без матана на примерах

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

30 сентября 2026 г.
660

Комментарии

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

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

Содержание

Почему 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: тестируйте не только в одном браузереКак внедрять доступность в процесс разработкиАвтоматизированный аудит — не замена, а стартовая точкаМинимальные ручные тесты, которые стоит сделать привычкойДокументируйте паттерны компонентовПрактический пример: доступный выпадающий список