Привет, Хабр!

Мне нравится смотреть, как верстают современные фронтендеры. Забавно наблюдать, как меняется вёрстка с годами. И сразу скажу, что не всё «плохо». Но ошибки, конечно же, есть. Раньше были свои примеры «плохого» кода, сейчас другие. О них хочу поговорить в этой статье.

Я составил список распространённых примеров кода «с душком». Старался быть объективным, но судить только вам, насколько это у меня получилось.

Давайте посмотрим, что я вам подготовил.

Графические кнопки без альтернативного текста

Десять лет назад я во всех статьях писал, что кнопки нужно размечать элементом button. Не элементом div. Сейчас ситуация стала лучше. Многие делают всё правильно. Так что я теперь буду писать много про альтернативный текст для интерактивных элементов.

Пожалуйста, всегда добавляйте его. У каждой кнопки и ссылки должен быть альтернативный текст. Даже если визуально его нет. Для демонстрации посмотрим ссылку на страницу поиска.

<a href="/search/" class="als-header-2021-nav-item als-header-2021-buttons-search" data-toggle="als-search">&nbsp;</a>

Почему это важно? Давайте посмотрим на то, как скринридер NVDA распознает такую ссылку. Только у меня есть ремарка. Я буду использовать отдельную страницу, где будет только она. Так будет проще продемонстрировать вам проблему.

В режиме «Список элементов» скринридер отображает подпись «Без метки» для ссылки. Это означает, что пользователь услышит слово «Ссылка». Всё. Дальше пользователь будет додумывать сам, куда же ведёт эта ссылка.

Теперь рассмотрим, как исправить ошибку. Есть два классических способа. Первый — это добавить альтернативный текст с помощью атрибута aria-label.

<body>
  <button type="button" aria-label="Выйти">
    <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
      <path d="M24 20v-4h-10v-4h10v-4l6 6zM22 18v8h-10v6l-12-6v-26h22v10h-2v-8h-16l8 4v18h8v-6z"/>
    </svg>
  </button>
</body>

Второй — использовать паттерн «visually hidden».

<body>
  <button type="button">
    <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
      <path d="M24 20v-4h-10v-4h10v-4l6 6zM22 18v8h-10v6l-12-6v-26h22v10h-2v-8h-16l8 4v18h8v-6z"/>
    </svg>
    <span class="visually-hidden" role="presentation">Выйти</span>
  </button>
</body>
.visually-hidden {
  clip-path: inset(50%);
  height: 1px;
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}

Отдельно отмечу, что в этом способе нужно также использовать атрибут role со значением presentationЯ более подробно описал причины данного решения в отдельной статье. Настоятельно рекомендую посмотреть. Также если вы не знакомы с атрибутом role, то и на этот случай есть статья.

Чем отличаются два подхода? В целом они дают одинаковый результат. Нюансы могут быть при переводе страницы, в частности, значение атрибута aria-label не переводится на другой язык в некоторых старых версиях браузеров и скринридеров.

По этой причине мой любимый вариант — паттерн «visually hidden». Поскольку текст в этом случае является частью контента страницы, проблемы с его переводом нет. По крайней мере, я никогда не встречал её.

Но если вам нравится решение с атрибутом aria-label, то проверьте, пожалуйста, как работает автоперевод. Чтобы ваш интерфейс не приводил к проблемам.

Анимация движения объекта без медиа-функции prefers-reduced-motion

Представим, что вам нужно реализовать анимацию объекта. Сначала он невидим, а потом, плавно увеличиваясь, появляется. А потом обратно также плавно исчезает. Анимация длится бесконечное количество раз.

<body>
  <div class="awesome-block"></div>
</body>
.awesome-block {
  width: 2rem;
  height: 2rem;
  background-color: purple;

  animation: zoomIn 1s ease-out alternate infinite both;
}

@keyframes zoomIn {
  0% { scale: 0; }
  100% { scale: 1; }
}

Пожалуйста, не надо так. Плавное движение объектов может укачивать пользователя. Например, моя подруга не может долго смотреть на нашу анимацию. Её начинает тошнить.

Давайте исправим наш код. Сделать это очень просто. Нам поможет медиа-функция prefers-reduced-motion. Обернём ей ту часть кода, которая отвечает за анимацию.

.awesome-block {
  width: 2rem;
  height: 2rem;
  background-color: purple;
}

@media (prefers-reduced-motion: no-preference) {
  .awesome-block {
    animation: zoomIn 1s ease-out alternate infinite both;
  }

  @keyframes zoomIn {
    0% { scale: 0; }
    100% { scale: 1; }
  }
}

Основная задача медиа-функции prefers-reduced-motion — проверить настройки операционной системы.

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

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

Подсказки для ат��ибута aria-label на неосновном языке страницы

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

Многие библиотеки адаптируют свои решения под скринридеры. Часто в таких решениях используется атрибут aria-label.

Через него создаются специальные подсказки, с помощью которых пользователь ориентируется. Другими словами, это важная штука. Так вот, проблема в том, что я постоянно вижу, как разработчики забывают переводить их на основной язык интерфейса.

Например, в русскоязычном интерфейсе используется подсказка «Next slide» на английском языке.

<div class="portfolio-slider__next" tabindex="0" role="button" aria-label="Next slide" aria-controls="swiper-wrapper-d9e1038c7067b7b4e" aria-disabled="false"></div>

В результате скринридер произнесёт её, но сделает это максимально непонятно. Пользователю придётся дополнительно тратить усилия, чтобы разобраться.

Я понимаю, что такая ошибка случилась из-за невнимательности разработчика. Поэтому, пожалуйста, следите за такими моментами. А в нашем примере я бы использовал подсказку «Следующее фото».

<!-- Я использую демонстрацию плагина, поэтому оставил код с использованием элемента div. Я настоятельно не рекомендую это решение. Используйте элемент button --> 
<div class="portfolio-slider__next" tabindex="0" role="button" aria-label="Следующее фото" aria-controls="swiper-wrapper-d9e1038c7067b7b4e" aria-disabled="false"></div>

Добавление контен��а с помощью свойства content

Ко многим ошибкам я отношусь спокойно. Только это не относится к добавлению контента на страницу с помощью свойства content. Если я это вижу в коде, то у меня загорается. Дымит не хило.

Сразу перейду к примеру. Для демонстрации вставим текст на страницу.

body::before {
  content: "Этот текст вставлен через CSS";
}

Проблема заключается в том, что этот тест будет озвучен скринридерами. Например, NVDA скажет: «Этот текст вставлен через CSS».

Конечно же, именно такой код вы не встретите. А вот использование свойств content и counter можно встретить в очень многих статьях и видео. Я постоянно вижу его в списках лучших техник CSS.

Давайте посмотрим, как скринридер NVDA распознает пример с таким подходом. Я подготовил разметку, в которой есть раздел с заголовком и его номером.

<body>
  <section class="section" aria-labelledby="section-heading">
	  <h2 id="section-heading" class="section__heading">Обо мне</h2>
	  <div class="section__content">
		  <p>Я люблю доступные интерфейсы. Можете обращаться ко мне. Все расскажу и покажу. Денег много попрошу</p>
	  </div>
  </section>
</body>
.section {
  counter-increment: section-counter;
}
.section__heading::before {
  content: "0" counter(section-counter);
}

Скринридер NVDA скажет: «Заголовок уровень два. 01 Обо мне». Перед каждым заголовком будет озвучиваться его номер. И это плохо, потому что номер заголовка — это визуальная штука. Обычно дизайнеры делают их, чтобы было красиво.

Информативной части обычно в них нет. По этой причине пользователи скринридеров отнесут такую информацию к спаму, и лучше её скрыть.

<body>
  <section class="section" aria-labelledby="section-heading">
	<h2 id="section-heading" class="section__heading">
      <span aria-hidden="true">01</span>
      Обо мне
    </h2>
	<div class="section__content">
	  <p>Я люблю доступные интерфейсы. Можете обращаться ко мне. Все расскажу и покажу. Денег много попрошу</p>
	</div>
  </section>
</body>

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

Скрытие интерактивных элементов с помощью свойств pointer-events и opacity

Как можно скрыть кнопку? Казалось бы, задача супер простая. Я никогда не думал, что с этим могут возникнуть проблемы.

Как обычно, я ошибся. Покажу сразу, как разработчики скрывают кнопку.

<body>
  <button class="bad-button" type="button" disabled>Спрятать</button>
</body>
.bad-button {
  opacity: 0;
  pointer-events: none;
}

Для начала разберём решение подробнее. У кнопки объявлен атрибут disabled. Так разработчики отключили возможность попасть на неё с помощью клавиши Tab. Далее свойством pointer-events со значением none кнопку спрятали от кликов и тапов пользователя. И чтобы её ещё визуально нельзя было заметить, было добавлено свойство opacity.

Казалось бы, вот она идеально скрытая кнопка! Но нет. В данном решении не учитываются особенности взаимодействия пользователя скринридера. Они могут переключаться по элементам, используя клавиши стрелок. Так они смогут попасть на такую кнопку. Например, скринридер NVDA скажет: «Кнопка недоступна. Спрятать».

Самым надёжным способом спрятать элемент является свойство display со значением none. Если вы скрываете элемент, пожалуйста, старайтесь использовать его. Так вы резко снизите вероятность проблемы. Спасибо.

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

Заключение

Давайте подведём итог. В этой статье я рассмотрел следующие ошибки:

  • использование для атрибута aria-label альтернативных подсказок на неосновном языке веб-страницы;

  • реализацию интерактивных элементов без альтернативного текста;

  • добавление контента с помощью свойства content, в частности реализацию счётчиков с помощью свойства counter();

  • небезопасный подход к реализации анимации с движением объекта без медиа-функции prefers-reduced-motion;

  • скрытие интерактивных элементов с помощью свойств pointer-events и opacity.

Надеюсь, вы примете на заметку эти ошибки и не будете их совершать. Дополнительно хочу попросить вас поделиться ошибками, которые вы считаете критическими. Буду ждать их в комментариях.

На этом я прощаюсь. Спасибо за чтение!

P. S. Помогаю больше узнать про CSS в своём ТГ-канале CSS isn't magic. Присоединяйтесь. Ссылка в профиле.

© 2026 ООО «МТ ФИНАНС»