
Привет, Хабр!
При обучении разным языкам программирования всегда есть практики, которым не рекомендуется следовать. Это очень сильно помогает разработчикам избегать ошибок.
Только по какой-то причине сложно найти антипаттерны по языкам HTML и CSS. Может, потому что они не языки программирования?
В общем, у меня появилось желание это исправить. Я собрал несколько примеров, которые лично отношу к антипаттернам. Возможно, это субъективно, но надеюсь, что нет.
Давайте посмотрим, что я вам подготовил.
Элемент img без атрибута alt
Картинка является одним из самых сложных элементов, с которыми можно наделать проблем. Вот что вы будете делать, если вы не знаете, что написать в атрибут alt?
По опыту общения с разработчиками, большинство людей вообще не используют его.
<body>
<img src="mediastore/nn-banner.webp">
</body>
Это очень очень плохо. Пользователи скринридера будут вас проклинать. Вы создаёте огромную нагрузку на их слух таким кодом.
Дело в том, что если скринридер не видит у элемента img атрибута alt, то он начинает зачитывать значение из атрибута src. Всю указанную строку. Представьте, сколько спама слушают пользователи!
Если вы не знаете, что указать в качестве значения атрибута alt, то оставьте его пустым.
<body>
<img src="mediastore/nn-banner.webp" alt="">
</body>
В этом нет ничего страшного. Да есть ситуации, когда нужно всё же описать изображения. Но если изображение декоративное или не предоставляет нужную для пользователя информацию, то можно не указывать значение. Это лучше, чем удалить полностью атрибут.
Элемент section без привязанного заголовка
Изучая тему цифровой доступности веб-интерфейсов, я получил много инсайтов и разрушил немало мифов. Вот многие говорят, что HTML5-элементы полезны: скринридеры лучше понимают разметку с их помощью и дают больше полезных подсказок.
Я не спорю. В большинстве случаев это так. Но есть нюансы. Давайте рассмотрим пример, где используется несколько элементов section для разметки разделов страницы.
<body>
<section>
<h2>О нас</h2>
<!-- здесь контент раздела -->
</section>
<section>
<h2>Наши работы</h2>
<!-- здесь контент раздела -->
</section>
<section>
<h2>Контакты</h2>
<!-- здесь контент раздела -->
</section>
</body>
В общем, скринридеры не увидят их. Например, NVDA в режиме «Ориентиры» не увидит разделы страницы.

А JAWS скажет: «Области на странице не найдены».
Данную проблему нельзя решить при помощи одного HTML. Тут придётся использовать ARIA-атрибуты. В нашем случае подойдёт атрибут aria-labelledby.
Если вы не знакомы с ним, то тут нет ничего страшного и сложного. Атрибут aria-labelledby похож на атрибут for для элемента label. Он связывает элемент с другим элементом, в котором хранится его описание.
В нашем случае у каждого элемента section есть элемент h2, который описывает его. Вот их мы свяжем атрибутом aria-labelledby.
<body>
<section aria-labelledby="about-us-heading">
<h2 id="about-us-heading">О нас</h2>
<!-- здесь контент раздела -->
</section>
<section aria-labelledby="portfolio-heading">
<h2 id="portfolio-heading">Наши работы</h2>
<!-- здесь контент раздела -->
</section>
<section aria-labelledby="contacts-heading">
<h2 id="contacts-heading">Контакты</h2>
<!-- здесь контент раздела -->
</section>
</body>
Теперь посмотрим, как изменится результат в скринридерах. В NVDA в режиме «Ориентиры» мы увидим три элемента.

JAWS тоже отобразит их.

Я думаю, что эта проблема будет у большинства. Хочу немного поддержать. У меня тоже она была. До того, как я начал интересоваться темой цифровой доступности, я не знал, что нужно связывать элемент section с заголовком. Я думал, что всё будет работать само.
Так что не переживайте. Это упущение не ваше, а разработчиков стандарта. Просто, пожалуйста, поправьте ваш код и будет всё отлично!
Свойства justify-content и align-items без ключевого слова safe
Я лично не люблю, когда фронтендеры используют свойства justify-content и align-items для центрирования элементов. Особенно всплывающих, таких как модальные окна, тултипы и т. д.
.awesome-modal {
display: flex;
justify-content: center;
align-items: center;
/* оставшиеся CSS модального окна */
}
Этот код часто приводит к проблемам.
Дело в том, что браузеры при позиционировании не учитывают размеры родительского элемента. Это стандартное поведение для свойств justify-content и align-items. В итоге часть дочерних элементов может быть не отображена, когда их размеры будут больше, чем размеры родительского элемента.
В случае с модальным окном я часто встречаю, что кнопка «Закрыть» скрывается за пределами вьюпорта. Например, когда в нём находится длинная форма регистрации. Как раз она вызывает переполнение родительского элемента по дополнительной оси, за которую отвечает свойство align-items.
По этой причине лично я привык центрировать всплывающие элементы с помощью свойства margin со значением auto. Но, к сожалению, этот способ очень редко используется.
Я много лет рассказывал о нём. Спорил с коллегами. В общем, я сдался. Но, пожалуйста, добавьте в свой код ключевое слово safe для того направления, где, вероятнее всего, происходит переполнение контентом.
При стандартном направлении осей ключевое слово нужно объявить для свойства align-items.
.awesome-modal {
display: flex;
justify-content: center;
align-items: safe center;
/* оставшиеся CSS модального окна */
}
Если у вас изменены направления осей с помощью свойства flex-direction со значением column, то ключевое слово нужно добавить уже ко свойству justify-cotnent.
.awesome-modal {
display: flex;
justify-content: safe center;
align-items: center;
/* оставшиеся CSS модального окна */
}
Теперь элементы не потеряются. Всё будет работать без проблем в новых браузерах, а там, где свойство не поддерживается, всё останется без изменений.
Устанавливать начальные значения «переменных» в начале «блока»
Меня радует, что пользовательские CSS-свойства очень популярны среди разработчиков. Но мне кажется, что у нас до сих пор не сформированные лучшие практики по работе с ними. По этой причине каждый делает так, как может.
Учитывая, что во фронтенде много разработчиков, перешедших из других языков, они тянут привычки за собой. Например, они объявляют начальные значения «переменных» в начале «блока».
.awesome-block {
--banner-height: 2rem;
--banner-gap: 0.5rem;
--banner-safe-space: 1.5rem;
display: block;
min-height: var(--banner-height);
padding-right: calc(var(--banner-gap) + var(--banner-safe-space));
background-color: tomato;
}
@media (width > 1025px) {
.awesome-block {
--banner-height: 5rem;
--banner-gap: 0.75rem;
--banner-safe-space: 1.5rem;
}
}
По моему мнению, этот код очень легко сломать. Значения «переменных» в этом случае конкурируют между собой. Просто поменяйте правила местами, и они заработают совершенно не так, как задумано.
@media (width > 1025px) {
.awesome-block {
--banner-height: 5rem;
--banner-gap: 0.75rem;
--banner-safe-space: 1.5rem;
}
}
.awesome-block {
--banner-height: 2rem;
--banner-gap: 0.5rem;
--banner-safe-space: 1.5rem;
display: block;
min-height: var(--banner-height);
padding-right: calc(var(--banner-gap) + var(--banner-safe-space));
background-color: tomato;
}
Исправить такую проблему просто. Воспользуйтесь значением по умолчанию, чтобы задать начальное значение.
@media (width > 1025px) {
.awesome-block {
--banner-height: 5rem;
--banner-gap: 0.75rem;
--banner-safe-space: 1.5rem;
}
}
.awesome-block {
display: block;
min-height: var(--banner-height, 2rem);
padding-right: calc(var(--banner-gap, 0.5rem) + var(--banner-safe-space, 1.5rem));
background-color: tomato;
}
Всё! Это код вообще никак не сломать. Браузеры будут использовать значение по умолчанию, только если не будет объявлено другое значение для этого пользовательского свойства. Таким образом вы уберёте конкуренцию между значениями, и код перестанет ломаться.
Элемент label, находящийся после текстового поля
Плавающий лейбл у текстовых полей давно стал привычным паттерном. Существует множество примеров его реализации. Один из них — использование чистого CSS.
В такой реализации авторы предлагают использовать селектор + или ~.
.field__input:focus-visible + .field__hint {
/* здесь стили для плавающей метки */
}
Чтобы это правило сработало, нам нужно правильно расположить элементы. В результате может получиться так, что элемент label будет находиться после элемента input.
<body>
<form>
<div class="field">
<input id="email" type="email" class="field__input">
<label for="email" class="field__hint">E-mail</label>
</div>
<div class="field">
<input id="tel" type="tel" class="field__input">
<label for="tel" class="field__hint">Телефон</label>
</div>
</form>
</body>
Для объяснения проблемы давайте разберёмся с одним нюансом работы скринридера NVDA. Когда пользователь перемещается по странице с помощью клавиш стрелок и попадает на поле ввода, скринридер скажет ему: «Редактор».
Это вся подсказка. Я не буду оценивать работу NVDA. Нам главное понять, что мы получили неинформативную подсказку. И то, что в нашей разметке элемент label связан с текстовым полем, не помогает в этой ситуации.
До тех пор, пока пользователь не войдёт в режим редактирования, нажав Space или Enter, скринридер не озвучит подсказку из элемента label. Именно поэтому его позиция играет критическую роль.
Если бы в нашем примере элемент label находился перед элементом input, то пользователь прослушал бы подсказку перед тем, как попасть на поле. И он уже знает, какие данные в него вводить.
<body>
<form>
<div class="field">
<label for="email" class="field__hint">E-mail</label>
<input id="email" type="email" class="field__input">
</div>
<div class="field">
<label for="tel" class="field__hint">Телефон</label>
<input id="tel" type="tel" class="field__input">
</div>
</form>
</body>
В этом примере он сначала попадает на элемент label. Скринридер скажет: «Имеил». Далее пользователь нажмёт клавишу вниз (↓). Скринридер скажет: «Редактор». Ещё раз нажмёт клавишу вниз. Скринридер скажет: «Телефон». И ещё один раз нажмёт клавишу вниз. Скринридер скажет: «Редактор».
Получается, пользователь скринридера всегда знает, что ему вводить, не заходя в режим редактирования. Для него снижается непредсказуемость интерфейса, а следовательно, и ментальная нагрузка.
Заключение
Давайте подведём итог. В этой статье мы рассмотрели:
проблемы с элементом
img, если не указывать ему атрибутalt;подход с ключевым словом
safeдля улучшения центрирования всплывающих элементов;как помочь скринридерам найти разделы страницы, размеченные с помощью элемента
section;подход к объявлению значений для «переменных» с использованием значений по умолчанию;
почему элемент
labelдолжен находиться перед текстовым полем.
На этом всё. Спасибо за чтение!
P. S. Помогаю больше узнать про CSS в своём ТГ-канале CSS isn't magic. Присоединяйтесь. Ссылка в профиле.
© 2025 ООО «МТ ФИНАНС»
Комментарии (4)

ifap
05.11.2025 11:15Пользователи скринридера будут вас проклинать. Вы создаёте огромную нагрузку на их слух таким кодом.
Дело в том, что если скринридер не видит у элемента img атрибута alt, то он начинает зачитывать значение из атрибута src. Всю указанную строку. Представьте, сколько спама слушают пользователи!Может им стоит проклинать разрабов скринридеров, которые с бараньим упорством пытаются озвучить то, что озвучивать не требуется, и доходят до маразма, озвучивая URL?
Alt- необязательный элемент, его отсутствие равнозначноalt=""Даже во WCAGaltтребуется указывать, если изображение несет смысловую нагрузку, а не чисто декоративное.
Tyusha
Не хватает уточнения, сколько людей пользуются скринридерами. Может сначала стоит задуматься об удобстве более многочисленных категориях пользователей.
ifap
Разве что-то из предложенного автором мешает жить большинству?
Tyusha
Нет, не мешает. Но может быть озаботиться более важными антипаттернами, коих прорва, а не докапываться до таких мелочей.