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

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

Вместо обычного ворчания я делюсь мыслями о том, как исправить ситуацию. Больше всего мне нравится показывать, что HTML и CSS могут помочь в этой задаче.

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

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

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

Значение balance для свойства text-wrap улучшит вашу типографику

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

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

<body>
  <main class="content">
    <h1>«Скучаю по «Краснодару». Сначала сделаешь, а потом думаешь». Интервью Ивана Игнатьева</h1>
    <p>Иван Игнатьев – первая яркая вспышка академии «Краснодара». </p>
    <p>Очень резво дебютировал в 18 лет: во второй игре забил «Ахмату» в РПЛ (победный на 3:2), в третьей – в квалификации Лиги Европы «Црвене Звезде» (первый из трех голов команды). За дебютные 24 матча в РПЛ – девять голов + пять голевых. А всего за «Краснодар» – 17 голов и шесть голевых в 54 матчах</p>
  </main>
</body>

Очень долгое время мне приходится грустно мириться с такой ситуацией. А теперь мы можем её исправить!

В CSS появилось значение balance для свойства text-wrap. Если браузеры встретят его, то начнут анализировать блок текста и делать так, чтобы на последней строке не было одиноких слов.

Давайте посмотрим, как значение balance изменит отображение заголовка из предыдущего примера.

h1:not([class]) {
  text-wrap: balance;
}

Свойство padding помогает пользователю проще попасть по элементу

Я люблю CSS за то, что он помогает легко улучшать пользовательский опыт. Даже самые очевидные свойства можно использовать для этого. Для примера возьмём свойство padding.

Казалось бы, задаёт оно внутренний отступ. Что тут полезного?

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

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

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

.navigation-burger-button {
  padding: 12px;
}

Они используют для кнопки свойство padding! Получается, они разделили пространство между логотипом и кнопкой на несколько видов. Часть задали свойством margin, а вторую часть — уже свойством padding.

.navigation-top__logo[data-v-63db22ea] {
  display: flex;
  height: 100%;
  flex-shrink: 0;
  align-items: center;
  margin-right: 16px
}

Также они используют такой подход для отступа между кнопкой и словом «футбол». Только тут свойство padding сочетается со свойством gap.

.navigation-burger__container {
  position: relative;
  display: flex;
  gap: 8px
}

Свойство aspect-ratio помогает избавиться от дублирования ссылок

Многие фронтендеры знают свойство aspect-ratio. Есть даже те, кто его использует, чтобы убрать прыжки контента при загрузке медиаконтента. А я вам пришёл сказать, что свойство aspect-ratio отлично борется с дублированием ссылок!

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

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

<body>
  <!-- другие элементы -->
  
  <article class="js-active js-material-list__blogpost material-list__item clearfix piwikTrackContent"> 
    <div class="material-list__item-text">  
      <time class="time-block time-block_top" datetime="2026-09-02 21:00:00">5 минут назад</time>   
      <h2 class="material-list__title" data-id="1117354790" data-title-id="" data-title-name="" data-material-position="1"> 
        <a class="h2 material-list__title-link" href="https://www.sports.ru/boxing/blogs/3457444.html" title="Вопрос на миллион."> Брат Хабиба говорит, что $1 млн – слишком мало. А что можно сделать на эти деньги? </a> 
      </h2> 
      <!-- здесь текст -->  
    </div> 
    
    <a class="material-list__item-img-link material-list__item-img-link_float_right" href="https://www.sports.ru/boxing/blogs/3457444.html" title="Вопрос на миллион.">  
      <img class="material-list__item-img" width="400" height="260" src="https://photobooth.cdn.sports.ru/preset/message/6/be/9b18485ed4ad69cdd91c2d92d52eb.jpeg?s=2x" alt="Брат Хабиба говорит, что $1 млн – слишком мало. А что можно сделать на эти деньги?" loading="lazy">  
    </a>
  </article>

  <!-- другие элементы -->
</body>

К какой проблеме приводит такая разметка?

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

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

Давайте теперь покажу, как исправить эту ситуацию. Для начала нам нужно создать псевдоэлемент ::before у первой ссылки. Он поможет нам сохранить интерактивность у изображения. Пользователь будет кликать по нему.

.material-list__item {
  position: relative;
}

.material-list__title-link::before {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
}

Мы будем позиционировать псевдоэлемент ::before с помощью свойства position со значением absolute. Его расположим справа относительно родительского элемента article. По этой причине я ему добавил свойство position со значением relative.

Далее нужно задать размеры. Разработчики используют значение 200px для превью.

.material-list__item-img-link_float_right {
  flex: 1 0 200px;
}

.material-list__item-img-link {
  width: 200px;
  height: 130px;
  /* другие свойства */
}

Поскольку псевдо-элемент находится в другом месте, то нам нужен способ передать туда значение. Мы будем использовать пользовательское свойство --material-preview-width.

.material-list__item {
  position: relative;
  --material-preview-width: 200px;
}

.material-list__item-img-link_float_right {
  flex: 1 0 var(--material-preview-width);
}

.material-list__item-img-link {
  width: var(--material-preview-width);
  height: 130px;
  /* другие свойства */
}

Далее добавим его к псевдо-элементу.

.material-list__title-link::before {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  width: var(--material-preview-width);
}

Теперь осталось рассчитать пропорции изображения и задать их с помощью свойства aspect-ratio. В нашем примере получилось значение 1.54.

.material-list__title-link::before {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  width: var(--material-preview-width);
  aspect-ratio: 1.54;
}

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

Осталось правильно расположить псевдоэлемент, рассчитав верхний отступ.

.material-list__title-link::before {
  content: "";
  position: absolute;
  right: 0;
  top: 8.75%;
  width: var(--material-preview-width);
  aspect-ratio: 1.54;
}

Осталось убрать ссылку. Я заменю элемент a на элемент div.

<body>
  <!-- другие элементы -->
  
  <article class="js-active js-material-list__blogpost material-list__item clearfix piwikTrackContent"> 
    <div class="material-list__item-text">  
      <time class="time-block time-block_top" datetime="2026-09-02 21:00:00">5 минут назад</time>   
      <h2 class="material-list__title" data-id="1117354790" data-title-id="" data-title-name="" data-material-position="1"> 
        <a class="h2 material-list__title-link" href="https://www.sports.ru/boxing/blogs/3457444.html" title="Вопрос на миллион."> Брат Хабиба говорит, что $1 млн – слишком мало. А что можно сделать на эти деньги? </a> 
      </h2> 
      <!-- здесь текст -->  
    </div> 
    
    <div class="material-list__item-img-link material-list__item-img-link_float_right">  
      <img class="material-list__item-img" width="400" height="260" src="https://photobooth.cdn.sports.ru/preset/message/6/be/9b18485ed4ad69cdd91c2d92d52eb.jpeg?s=2x" alt="Брат Хабиба говорит, что $1 млн – слишком мало. А что можно сделать на эти деньги?" loading="lazy">  
    </div>
  </article>

  <!-- другие элементы -->
</body>

Как лучше расположить кнопку «Закрыть»

Я часто говорю про привычки фронтендеров. Большинство из них сформировались неосознанно, и поэтому в них заложено огромное количество возможностей по улучшению пользовательского опыта.

Не верите? Давайте хотя бы посмотрим, как положение элементов в разметке влияет на людей. Сделаем это на примере модального окна авторизации на сайте. Мы рассмотрим два примера разметки. В каждом варианте посмотрим, где находится кнопка «Закрыть».

Первый кандидат — это сайт спортивных новостей.

<body>
  <!-- другие элементы -->
  
  <div data-v-81535de7="" class="ui-modal auth-popup__modal">
    <div data-v-81535de7="" class="ui-modal__head">
      <div data-v-81535de7="" class="ui-body-sm-regular">
        <div data-v-68ef455c="" class="auth-popup">
          <button data-v-68ef455c="" class="auth-popup__close">
            <!-- здесь иконка кнопки «Закрыть» -->
          </button>
          <div data-v-f9251e92="" data-v-68ef455c="" class="auth-popup__info">
            <!-- здесь логотип и дополнительные ссылки -->
          </div>
          <div data-v-68ef455c="" class="auth-screen auth-screen--auth">
            <div class="auth-screen__screen">
              <div class="auth-screen__title">Авторизация</div>
              <!-- здесь блок с соц.сетями и формой авторизации -->
            </div>
          </div>
        </div>        
      </div>
    </div>
  </div>

  <!-- другие элементы -->
</body>

Когда пользователь скринридера захочет авторизоваться на сайте, то первым для него элементом будет кнопка «Закрыть». Так получится, потому что в разметке элемент button находится первым, если не считать элементы div.

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

В качестве второго примера мы будем использовать сайт с личными финансовыми историями. Также посмотрим разметку модального окна авторизации.

<body>
  <!-- другие элементы -->

  <div class="_modal_a85da_3" role="dialog" aria-modal="true" aria-labelledby="dialog-title" aria-describedby="dialog-description" data-fixed-on-top="true" data-placement="center" data-disable-scroll="true">
    <div class="_content_a85da_90" data-auto-width="false" data-has-vertical-padding="true" data-has-horizontal-padding="true">
      <div class="_title_a85da_150" id="dialog-title">
        <h2 class="_heading_1put6_1" data-level="2">Вход в&nbsp;Т—Ж</h2>
      </div>
      <div class="_description_a85da_160" id="dialog-description">
        <!-- здесть текст -->
      </div>
      <form class="_container_1b8uw_1">
        <!-- здесть форма авторизации -->
      </form>
      <div class="_container_zbz87_1">
         <!-- здесть соц.сети -->
      </div>
      <p class="_privacy_1b8uw_63">
        <!-- здесть текст -->
      </p>
      <button type="button" aria-label="Закрыть окно" data-button-type="default" class="_close_a85da_68">
        <!-- здесть иконка кнопки «Закрыть» -->
      </button>
    </div>
  </div>

  <!-- другие элементы -->
</body>

Мы видим, что кнопка «Закрыть» находится последним элементом. Пользователи скринридера увидят её, только пройдя через все основные части модального окна. Там, где они и ожидали её встретить.

Функция contrast-color() поможет вам просто стилизовать выделенный текст

Многие забывают о том, что выделение текста можно изменить. Да, вместо голубого фона и белого текста можно использовать любые цвета! Вот вам и мой последний совет. Шучу.

А какой цвет использовать? Дизайнеры тоже забывают стилизовать выделение текста. Даже если вы попросите их сделать это, то не факт, что быстро получите результат. И тут единственный человек, который быстро выполнит задачу, — это вы.

А главное, вам не нужно подбирать цвет самому. Для фона всё относительно просто. Используйте основной цвет в приложении. А вот подобрать цвет текста уже сложнее. Коллеги, не переживайте. За вас всё сделают браузеры!

В CSS есть функция contrast-color(). Она автоматически подбирает цвет текста, который хорошо контрастирует с фоном. Именно её мы и будем использовать.

Нам достаточно передать в неё значение цвета, которое используется для свойства background-color.

:root {
  --selection-background-color: purple;
}

::selection {
  background-color: var(--selection-background-color);
  color: var(--selection-text-color, contrast-color(var(--selection-background-color)));
}

Мы получили белый текст на пурпурном фоне. Идеальная пара по контрасту.

Также хочу предупредить вас. У функции contrast-color() есть ограничение. Она в основном передаёт либо значение #ffffff, либо #000000. Это может привести к проблемам у пользователей, у которых есть фотофобия (светобоязнь).

По этой причине я создал переменную --selection-text-color. Она поможет вам переопределить значение в будущем, если у вас появятся пользователи с фотофобией или дизайнер захочет подобрать другой цвет.

Заключение

Давайте подведём итог. В этой статье мы рассмотрели:

  • где должна располагаться кнопка «Закрыть» в компонентах, в которых есть возможность закрыть элемент;

  • значение balance для свойства text-wrap, помогающее избежать одиноких слов на последней строке в блоке текста;

  • как свойство padding увеличивает кликабельную область интерактивного элемента, упрощая пользователю возможность по нему попасть;

  • свойство aspect-ratio для устранения дублирования ссылок в блоках, где есть превью и заголовок;

  • использование функции contrast-color() в качестве значения по умолчанию для стилизации выделенного текста с помощью псевдоэлемента ::selection.

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

На этом всё. Спасибо за чтение!

P. S. Помогаю больше узнать про CSS и дружелюбные интерфейсы в своих ТГ‑каналах CSS isn't magic и UX + Dev = a11y. Присоединяйтесь. Как вступить, написано в профиле.

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

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