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

При обучении разным языкам программирования всегда есть практики, которым не рекомендуется следовать. Это очень сильно помогает разработчикам избегать ошибок.

Только по какой-то причине сложно найти антипаттерны по языкам 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 ООО «МТ ФИНАНС»

Комментарии (31)


  1. Tyusha
    05.11.2025 11:15

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


    1. ifap
      05.11.2025 11:15

      Разве что-то из предложенного автором мешает жить большинству?


      1. Tyusha
        05.11.2025 11:15

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


        1. melnik909 Автор
          05.11.2025 11:15

          более важными антипаттернами, коих прорва,

          Отлично! Я без сарказма. Можете сказать, какие бы вы написали? Буду признателен


          1. Tyusha
            05.11.2025 11:15

            Я не профи, поэтому не писательница, а читательница подобных материалов. Из того, что бесит:

            1. Стоэтажные div, нагенерённые непойми-каким фреймворками.

            2. Использование CMS и фреймворков к месту и не к месту. Если у вас статичный лендинг, почему не написать его на flat html с вложенностью тэгов не больше трёх-четырёх, а обработку какого-нибудь единственного клика не сделать на vanilla js без рендеров, хуков и роутинга. Сейчас подобные предложения воспринимаются как: "переписать всё на ассемблере".


            1. melnik909 Автор
              05.11.2025 11:15

              спасибо!


            1. POPSuL
              05.11.2025 11:15

              А вам прям спать не дают, эти многоэтажные дивы?


    1. askurashev
      05.11.2025 11:15

      «Никто не думает о том, что его не касается, не касается сейчас». Думать о людях с ограниченными возможностями важно, несмотря на то, что это затронет малую часть общества.


      1. melnik909 Автор
        05.11.2025 11:15

        Вот исследование Яндекса по поводу использования настроек "доступности" https://inclusion.yandex.ru/settingsresearch.

        Доступность не про малую часть общества. Она про всех нас. Просто есть более "заметные" примеры, есть менее.


  1. ifap
    05.11.2025 11:15

    Пользователи скринридера будут вас проклинать. Вы создаёте огромную нагрузку на их слух таким кодом.
    Дело в том, что если скринридер не видит у элемента img атрибута alt, то он начинает зачитывать значение из атрибута src. Всю указанную строку. Представьте, сколько спама слушают пользователи!

    Может им стоит проклинать разрабов скринридеров, которые с бараньим упорством пытаются озвучить то, что озвучивать не требуется, и доходят до маразма, озвучивая URL? Alt - необязательный элемент, его отсутствие равнозначно alt="" Даже во WCAG alt требуется указывать, если изображение несет смысловую нагрузку, а не чисто декоративное.


    1. entze
      05.11.2025 11:15

      Разрабам скринридеров кажется пора прикручивать локальные Vision модели с сегментацией объектов и человеческим TTS.
      Прописывать все что рекомендовано автором конечно полезно, но - это при ручной верстке. CMS вообще предполагают такие настройки? Ну может какие-то тяжелые системы у которых accessability обязан быть.


      1. ifap
        05.11.2025 11:15

        Если тэг ставится шаблоном, то в CMS можно наворотить чего душе угодно, вот если ручками вставляется, тадыой, хотя опять же, можно, скажем, дефолтно вставлять пустой alt


      1. gun_dose
        05.11.2025 11:15

        CMS вообще предполагают такие настройки?

        Drupal по умолчанию делает атрибут alt обязательным для заполнения. Чтобы сделать его необязательным, надо заходить в настройки и менять.

        прикручивать локальные Vision модели

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

        Кстати, тот же Drupal умеет автоматически генерировать альты, через интеграцию с внешними ИИ-сервисами. Это куда более рационально - один раз распознать и сохранить, чем многократно распознавать одни и те же картинки на клиентских девайсах.


    1. melnik909 Автор
      05.11.2025 11:15

      Может им стоит проклинать разрабов скринридеров, которые с бараньим упорством пытаются озвучить то, что озвучивать не требуется, и доходят до маразма, озвучивая URL?

      Как я понял, это малоперспективное занятие


      1. ifap
        05.11.2025 11:15

        В итоге имеем то, что имеем - с больной головы на здоровую.


    1. vanxant
      05.11.2025 11:15

      Официальный валидатор считает атрибут alt обязательным и ругается на его отсутствие.


      1. nikolayshabalin
        05.11.2025 11:15

        Он всё верно говорит, так как нужно не отсутствия атрибута, а именно пустой alt="", без пробелов


  1. dom1n1k
    05.11.2025 11:15

    Не согласен с пунктом про переменные в начале блока.

    Дело в том, что CSS в принципе так устроен, что порядок свойств имеет значение. Это примерно как сказать, что python-код легко поломать, просто добавив или убрав несколько отступов - да, но это не баг, а фича. Поэтому де-факто есть общепринятое правило - все медиа-запросы всегда идут после умолчального селектора. Если я вижу медиа-запрос до - что-то тут не так.

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


    1. Kuch
      05.11.2025 11:15

      Я бы сказал, что в принципе в большинстве языков программирования если объявление переменных менять местами, то будет разное значение


  1. alexnozer
    05.11.2025 11:15

    В будущем, когда поддержка селектора :has() станет лучше, плавающие подписи можно будет делать с сохранением порядка <label> - <input>. Сейчас это обусловлено реализацией через + или ~. Со временем можно будет делать так:

    label:has(+ input:is(:not(:placeholder-shown), :focus))


    1. melnik909 Автор
      05.11.2025 11:15

      Алексей, спасибо за комментарий. Только вместо :focus надо :focus-visible


      1. alexnozer
        05.11.2025 11:15

        Да, согласен, лучше :focus-visible использовать


  1. vanxant
    05.11.2025 11:15

    Ну, с тем, что скринридеры не могут найти заголовок внутри секции, пусть разбираются разрабы скринридеров. Оно как бы по стандарту обязательно.

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


    1. melnik909 Автор
      05.11.2025 11:15

      У меня к вам вопрос. Скажите, пожалуйста, почему вы читаете мои статьи?

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

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

      Потом мне стало интересно узнать вашу цель. Все же на протяжении 2 лет вы оставляете такой комментарий под моими такими статьями. Если вы просто хейтили бы, это было бы понятно. Но вы продолжаете читать статьи. Это же не просто так все!

      Заранее благодарю за ответ!


      1. vanxant
        05.11.2025 11:15

        Понимаете, хабр — не чья-то личная площадка (не моя, не ваша и т.д.). Это чуть ли не единственный источник годного технического контента на русском, по крайней мере в сфере IT. Он будет в первых строчках выдача гуглояндекса почти по любому запросу, если есть подходящая статья. Всё остальное это либо машинный пересказ/перевод so, reddit и hackernews, либо чьи-то нерецензируемые бложики с рекламой курсов С++ за 21 день или хотя бы "вот моя телега".

        А хабр по факту рецензируемый. Ценен он именно комментариями к статьям, которые часто полезнее самой статьи и где выражают в т.ч. мнения, отличные от мнения автора. Эти альтернативные взгляды как минимум многих читателей заставят задуматься А среди читателей могут быть как джуны, так и преподы разных там институтов и даже участники групп по разработке стандартов языков, ГОСТов и пр.

        Считайте, что я добавляю вашим статьям ценности:)

        PS. К вам лично никаких претензий или ещё какого неровного отношения я не имею. В статьях про какие-нибудь плюсы комментирую точно так же и с той же целью.


        1. melnik909 Автор
          05.11.2025 11:15

          Я согласен с вашим видением Хабра. Мой вопрос был личным. Вы постоянный читатель моих статей. Всего на Хабре таких у меня 5 человек. Как я говорил, вы читаете меня минимум 2 года.

          Для меня это уже ценно! Вы месяц назад не прокомментировали статью, я уже думал, а где вы? Это без шуток было так.

          Мне и интересно узнать, какая ваша мотивация читать то, с чем вы чаще не согласны. У меня такое впечатление сложилось по вашим комментариям. Я могу ошибаться в этой оценке. Поэтому я уточняю.


      1. youngmysteriouslight
        05.11.2025 11:15

        Безотносительно вашего многолетнего противостояния, хочу заметить, что меня тоже возмутила подача материала в статье по некоторым вопросам (собственно, по h2-section и по for-label).

        Если стандарт требует, чтобы семантическая разметка была такой-то (скажем, чтобы label шел перед input), то и аргументация должна звучать как «это требуется стандартом и точка» без упоминания того, как там скринридеры или иные браузеры себя ведут или не ведут. Если же стандарт говорит «section должен содержать заголовок» и он содержит, а скинридеры его не умеют подвязывать, то это ошибка разработчиков скринридера и пусть их поносят пострадавшие, доколе не исправят.

        Это мне напоминает времена, когда стандарты уже приняты, но MS и некоторые другие браузеры привносили отсебятину. Некоторые сайты на неё опирались. Эти сайты не открывались в нормальных браузерах, которые действовали по стандарту. Кого винили простые пользователи? Браузер, в котором сайт не работает, а не сайт, написанный альтернативно-одарёнными разработчиками, не умеющими в стандарт. Это не нормальная ситуация. И сейчас подобному потакать тоже не следует.


        1. kspshnik
          05.11.2025 11:15

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

          Так что это в наших интересах - делать удобные и полезные сайты :)

          Это ес-сно не отменяет необходимости посыпать разработчиков скринридеров словом добрым и незлобливым в формате issues. Но оное посыпание не заменяет учёта текущей ситуации.


      1. vanxant
        05.11.2025 11:15

        считая деньги за написание атрибута

        А давайте, кстати, посчитаем.

        Контент (статьи, описания товаров и т.д.) обычно пишется всё-таки или в wysiwyg-редакторе, или в ворде и потом копируется в wysiwyg-редактор.

        Уже поняли, к чему я клоню, да? Даже не рассматривая вариант, что автор выделяет заголовки большим полужирным шрифтом, а не заголовками.

        Т.е. вы предлагаете распарсить html из ворда, найти там заголовок (какой, если их несколько?) и навесить на него id, чтобы потом можно было сослаться.

        Ага, ага, уже представляю директора веб-студии, который даёт на это денег ради кривых скрин-ридеров.


        1. melnik909 Автор
          05.11.2025 11:15

          Уже поняли, к чему я клоню, да? 

          Вы ловко привели пример в свою сторону. И вы также знаете, что большинство элементов section верстаются вручную разработчиком.

          Я давно говорю о том, что по мере возможности улучшаем, что можем. Вы почему-то хотите видеть то, что я говорю "а давайте скажем бизнесу мы будем допиливать проект под скринридер". Понятно, что это выстрел себе в голову, и так это не работает.

          И вы ушли от вопроса. Так почему вы читаете мои статьи 2 года?


          1. vanxant
            05.11.2025 11:15

            По второму вопросу - ну, я читаю статьи на хабре, когда есть время.

            По первому, извините, не согласен. Как раз вёрстку макета в целом в 99.9% делает верстальщик, а вот контент внутрь section запихивает контент-менеджер через wysiwyg редактор. Даже если речь про одноразовый лендинг, тексты 100 раз согласовываются и меняются.

            И тут классическая ситуация, когда разрабы скринридеров пытаются скинуть свою работу (найти заголовок внутри секции) на миллион верстальщиков. Нет, ребята, нафиг — это туда:)