
Привет, Хабр!
Мне нравится смотреть, как верстают современные фронтендеры. Забавно наблюдать, как меняется вёрстка с годами. И сразу скажу, что не всё «плохо». Но ошибки, конечно же, есть. Раньше были свои примеры «плохого» кода, сейчас другие. О них хочу поговорить в этой статье.
Я составил список распространённых примеров кода «с душком». Старался быть объективным, но судить только вам, насколько это у меня получилось.
Давайте посмотрим, что я вам подготовил.
Графические кнопки без альтернативного текста
Десять лет назад я во всех статьях писал, что кнопки нужно размечать элементом button. Не элементом div. Сейчас ситуация стала лучше. Многие делают всё правильно. Так что я теперь буду писать много про альтернативный текст для интерактивных элементов.
Пожалуйста, всегда добавляйте его. У каждой кнопки и ссылки должен быть альтернативный текст. Даже если визуально его нет. Для демонстрации посмотрим ссылку на страницу поиска.

<a href="/search/" class="als-header-2021-nav-item als-header-2021-buttons-search" data-toggle="als-search"> </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 ООО «МТ ФИНАНС»
Комментарии (23)

UniInter
17.02.2026 10:34Про разницу между opacity:0; и display:none; надо бы добавить следующее:
opacity:0; место элемента остается нетронутым, но на его месте ничего не отображается.
display:none; место элемента съедается.
opacity:0; участвует в рендере страницы в отличие от display:none; и, значит, оказывает бОльшую нагрузку на движок, что может сказываться на большОм кол-ве элементов с таким свойством.

ArtyomOchkin
17.02.2026 10:34Укачивать?
Вкусовщина. Лично меня раздражает дёргающийся кадр, когда при пролистывании (мышью или пальцем) передо мной происходит грубый scroll. Или с задержкой. Раньше наоборот стремились к оптимизации и плавности, а теперь...
Честно, впервые такое слышу, не считая похожей стаи на Хабре, которую читал буквально в конце декабря, ссылки увы под рукой не сохранилось.
А вот проблема корректности интерфейса для слабовидящих, действительно серьёзный вопрос

vcKomm
17.02.2026 10:34Меня раздражает любой перехват скролла на всех этих невероятных дизайнерских сайтах. Я не могу скроллить со скоростью, которой хочу, а только с той, которую мне разрешают

ArtyomOchkin
17.02.2026 10:34Согласен. Это ненормальное явление...
Помнится, раньше сайтоделы экспериментировали ещё и с перехватом курсора, кажется даже в рамках сайтов. Точно, как и перехват скролла в наши дни, раздражало, да и тормозов добавляло.

d3d11
17.02.2026 10:34Антипаттерн: телефонный дизайн на многих сайтах - скролл экранами. Первый экран - надпись "наш замечательный сайт", бекгроунд, картинки. Скроллим вниз на экран, следующий экран - надпись №2. И т.д.

artptr86
17.02.2026 10:34Это, скорее, «презентационный» дизайн

d3d11
17.02.2026 10:34Если бы. Заметил, что вполне сервисные сайты так делают. Возможно связано с новомодным зумерским "клиповым мышлением".

Darkness_Paladin
17.02.2026 10:34"Клипового мышления" не существует, это миф от любителей (и производителей) нудноты.
А мода на сайты, листаемые экранами — это чтоб было не слишком заметно, что полезной информации на таком "сайте" едва ли сотня слов )))

grimcap
17.02.2026 10:34Скрытие элемента при помощи display:none не позволит по нормальному сделать анимацию плавного появления без js
Статья должна была называться "Плохие практики в вёрстке под скринридеры"
Потому что со скринридеров заходят не более 1.5% людей (https://qna.habr.com/q/586772)
А сколько людей со скринридов будут заходить на сайт. чтобы купить подъемник в ресторан?Гораздо полезнее на мой взгляд делать на сайте возможность включить светлую тему. Или по крайней мере включать темную тему, если оно задано браузером. Темной темой пользуется до 80% пользователей, а на приведенном сайте ее нет (что логично для сайта-визитки, тем не менее). Зато есть jQuery в 2026 году.

Metotron0
17.02.2026 10:34не позволит по нормальному сделать анимацию плавного появления без js
Уже кое-что придумали

Extremum
17.02.2026 10:34Вообще все перечисленное относится к игнорированию требований доступности, коих гораздо больше, чем описано в статье.

Femistoklov
17.02.2026 10:34Плохие практики в вёрстке
Ожидал увидеть популярные по историческим причинам практики, у которых есть лучшие более простые альтернативы. А тут всё про скринридеры (это что-то, связанное с доступностью)?
Формулируйте, пожалуйста, названия статей так, чтобы они не вводили в заблуждение.

ts347
17.02.2026 10:34В статье рассмотрены какие-то сложные вещи. Вот куда более простые и распространенные.
Вводишь номер телефона для авторизации, нажимаешь Enter и ничего не происходит. Привет, Ozon.
Перемещаться по элементам страницы можно с помощью стрелок на клавиатуре, вот только разработчики забыли, что стрелки есть еще и на нампаде. Привет Яндексу.

melnik909 Автор
17.02.2026 10:34Перемещаться по элементам страницы можно с помощью стрелок на клавиатуре, вот только разработчики забыли, что стрелки есть еще и на нампаде. Привет Яндексу.
Скажите, пожалуйста, как эта проблема относится к верстке (HTML/CSS)?

Darkness_Paladin
17.02.2026 10:34с помощью стрелок на клавиатуре, вот только разработчики забыли, что стрелки есть еще и на нампаде.
С точки зрения вменяемого разработчика, стрелок нет ни на нампаде, ни где-то ещё -- стрелки ПРОСТО ЕСТЬ, как таковые. Пришло событие с key="ArrowUp" -- обрабатываем именно ArrowUp, не глядя в code (для основной стрелки code="ArrowUp", для нумпадной code="Numpad8") и не проверяя location (0 для основной, 3 для нумпада).
Возможность различать основные стрелки и стрелки на нумпаде нужна только для специальных целей -- в основном для игр: чтоб случайное нажатие кнопки намлок не сделало недоступными функции, подвешенные на кнопки нумпада, игра должна читать из событий клавиатуры не key (который зависит от модификаторов и раскладки), а code (который привязан именно к конкретной клавише на клавиатуре и не меняется от переключения языка, раскладки или режима, или нажатия клавиш-модификаторов.).
А вообще, правильный сайт вообще не должен ничего делать сам по нажатию на стрелки, это функция браузера и/или "средств доступности", разработчик должен только правильно разметить страницу, чтоб эти средства правильно работали.
ЗЫ. Сейчас заглянул на яндекс и вообще не понял, о чём вы. Нет там никакого управления с клавиатуры, кроме обеспечиваемого браузером -- а на сайте яндекс-погоды и его сломали, там активные (выбранные табом) ссылки не выделяются курсорной рамкой.

ts347
17.02.2026 10:34Яндекс большой. Я тоже не особо понял, куда вы заглянули.
В веб-интерфейсе яндекс-почты стрелками можно перемещаться в списке писем и папок, пробел выделяет, Delete удаляет. На цифровом блоке стрелки и Delete не работают.
В яндексовских электронных таблицах стрелки перемещают фокус по ячейкам, как в Excel. Стрелки на нампаде не работают.
В яндекс-музыке стрелки вправо-влево перематывают, вверх-вниз — громкость. Стрелки на нампаде... думаю, уже понятно.
правильный сайт вообще не должен ничего делать сам
Не согласен. Самый лучший сайт — тот, которым удобно пользоваться. Если для этого нужно переопределять системное поведение — что ж. У того же яндекса с этим в целом всё хорошо, но можно лучше.
P.S. Да, это не про верстку. В целом про юзабилити.

Darkness_Paladin
17.02.2026 10:34В веб-интерфейсе яндекс-почты стрелками можно перемещаться
Понял. Да, действительно, есть такая штука, и действительно криво сделана. Ну так, шо вы хотите, там React ))) (вероятно, я предвзят, но у меня к реакту ОЧЕНЬ плохое)
А с другой стороны, скажу честно -- я вообще не очень представляю, кто пользуется нумпадом в режиме курсора, сейчас вроде нет таких клавиатур, где есть нумпад, но нет блока 3-6-4.
Вероятно, этим балбесам из яндекса тоже в голову не пришло, что это кому-то нужно, вот и сделали функцию per rectum, через проверку на code, а не key.
Самый лучший сайт — тот, которым удобно пользоваться.
Ну, имхо, почта или электронные таблицы -- это уже ближе не у понятию сайта, а к веб-приложению, пусть даже и исполняется оно в браузере. Им да, вполне можно позволить больше, чем простому сайту.
Zenitchik
Полагаю, можно не уточнять. Анимация - сама по себе, как таковая, снижает юзабельность (usability) сайта.
А Вы уверены, что это проблема? По идее, если разработчик добавляет контент, то он ожидает, что контент будет обработан как контент.
В данном примере, я как пользователь именно это и ожидал услышать.
melnik909 Автор
Проблема в том, что разработчик считает визуальный, декоративный элементом контентом
Zenitchik
Ну, так об этом и надо говорить. А пример - неудачный. Автоматически добавленный номер раздела - это не декоративный элемент, а несущий смысловую нагрузку.
melnik909 Автор
Я же об этом и написал в статье. Дальше уже сам читатель делает выбор. Вы решили, что не декоративный. Значит с точкой зрения автора не согласны. Хотя он оперирует не своим личным мнением, а пересказывает опыт взаимодействия пользователей скринридеров.
Безусловно дело ваше.