Хабр, привет! Я частенько пишу про работу CSS, его неизвестные возможности и влияние на доступность. Кажется, этих направлений мало для меня. Теперь я хочу показать техники вёрстки, используемые мной постоянно.


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


Сегодня я расскажу:

  • как избавиться от соседнего родственного комбинатора + при реализации нестандартных чекбоксов и радиокнопок;
  • про свойство inset, сокращающее код на целых три строки;
  • мой сниппет для расширения интерактивной области у кнопок и ссылок;
  • стиль написания медиа-запросов, позволяющий сократить количество правил;
  • альтернативный способ центрирования элемента без свойства transform.

▍ Новый способ создания нестандартных чекбоксов и радиокнопок


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


В моей демонстрации будет использоваться следующая разметка:


<body>
  <div class="cutom-radio-button">
    <input id="rb-1" class="cutom-radio-button__input sr-only" type="radio" name="radio" checked>
    <label for="rb-1" class="cutom-radio-button__label">Вариант №1</label>
  </div>
  <div class="cutom-radio-button">
    <input id="rb-2" class="cutom-radio-button__input sr-only" type="radio" name="radio">
    <label for="rb-2" class="cutom-radio-button__label">Вариант №2</label>
  </div>
</body>

Сама стилизация радиокнопок происходит так.
.sr-only {
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  position: absolute;
  white-space: nowrap;
}

.cutom-radio-button {
  --custom-radio-button-size: 1rem;
  --custom-radio-button-gap: 1rem;
  --custom-radio-button-dot-size: 0.5rem;
  
  display: inline-flex;
  align-items: center;
  position: relative;
  isolation: isolate;
}

.cutom-radio-button::before {
  content: "";
  box-sizing: border-box;
  width: var(--custom-radio-button-size);
  height: var(--custom-radio-button-size);

  border: 1px solid hsl(0, 0%, 14%);
  border-radius: 100%;
  position: absolute;
  z-index: -1;
}

.cutom-radio-button__label {
  display: grid;
  padding-left: calc(var(--custom-radio-button-dot-size) + var(--custom-radio-button-gap));
}

.cutom-radio-button__label::before,
.cutom-radio-button__label::after {
  content: "";
  width: var(--custom-radio-button-dot-size);
  height: var(--custom-radio-button-dot-size);
  border-radius: 100%;
  opacity: 0;

  position: absolute;
  align-self: center;
  left: var(--custom-radio-button-dot-size);  
  transform: translateX(-50%);
  scale: 0;
  transform-origin: left center;
}

.cutom-radio-button__label::before {
  background-color: hsl(0, 0%, 14%);
  transition: 0.3s;
}

.cutom-radio-button__label::after {
  background-color: hsl(250, 100%, 44%);
  transition: 0.6s;
}


Осталось сделать последний шаг. Дописать код для состояний, когда на радиокнопке сфокусировались и отметили её. Как я говорил ранее, он будет основан на соседнем родственном комбинаторе +.


.cutom-radio-button__input:checked + .cutom-radio-button__label::before {
  opacity: 1;
  scale: 1;
}

.cutom-radio-button__input:focus + .cutom-radio-button__label::after {
  scale: 3.6;
  opacity: 0.2;
}

В чём проблема? Если в разметке элемент с классом .cutom-radio-button__input случайно перестанет быть перед элементом .cutom-radio-button__label, стилизация полетит к чёрту. В этом заключается проблема использования соседнего родственного комбинатора +.


В новой технике этого недостатка нет, потому что она основана на псевдоклассах :has() и :focus-within.


.cutom-radio-button:has(:checked) .cutom-radio-button__label::before {
  opacity: 1;
  scale: 1;
}

.cutom-radio-button:focus-within .cutom-radio-button__label::after {
  scale: 3.6;
  opacity: 0.2;
}

Оба псевдокласса срабатывают на родителе, когда их потомок получает какое-либо состояние. В случае псевдокласса :has() у потомка применяются стили, когда срабатывает состояние «отмечено». А когда на потомке сфокусировались, то в дело включается псевдокласс :focus-within.


По сути можно обойтись одним псевдоклассом :has(), заменив псевдокласс :focus-within.


.cutom-radio-button:has(:checked) .cutom-radio-button__label::before {
  opacity: 1;
  scale: 1;
}

.cutom-radio-button:has(:focus) .cutom-radio-button__label::after {
  scale: 3.6;
  opacity: 0.2;
}

Мне этот способ не нравится. Я любитель правил. Раз дали псевдокласс :focus-within, то надо его использовать. Но я не могу запрещать вам. Поэтому, если что, имейте в виду.


▍ Свойство inset — новый способ растянуть элемент


Иногда элементы со свойством position и значением absolute используются для растягивания элемента по всему доступному пространству. Для этого чаще всего используется следующий код:


.parent {
  position: relative;
}

.parent::before {
  content: "";
  width: 100%;
  height: 100%;
  
  position: absolute;
  top: 0;
  left: 0;
}

Это устаревший сниппет. Я так думаю, потому что его можно сократить. Поможет в этой задаче свойство inset. Оно задаёт координаты для элемента сразу с четырёх сторон. Другими словами, оно устанавливает значение для свойств top, right, bottom и left.


Возвращаясь к моему примеру, нужно сделать пару действий. Первое — удалить свойства width и height. Поскольку для элемента .parent применено свойство position со значением absolute, его размеры могут быть рассчитаны в зависимости от заданных координат отступа.


В нашем примере для него будем использовать значение 0, которое установлено для свойства inset.


.parent {
  position: relative;
}

.parent::before {
  content: "";
  position: absolute;
  inset: 0;
}

Где полезен этот сниппет? У меня как раз есть полезный пример, который позволит вам значительно улучшить интерфейс вашего продукта. Его рассмотрим далее.


▍ Сниппет расширения кликабельной области


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


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


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


.ha-clickable-area {
  position: var(--ha-clickable-area-position, relative);
  isolation: isolate;
}

.ha-clickable-area::before {
  content: "";
  position: absolute;
  inset: calc(-1 * var(--ha-clickable-area-expandable-ratio));
  z-index: -1;
}

Вся фишка заключается в коэффициенте расширения области --ha-clickable-area-expandable-ratio. Он отвечает за то, на сколько пикселей псевдоэлемент выйдет за пределы родителя, т. е. на сколько пикселей кликабельная область расширится.


Как применять сниппет, я покажу на моём недавнем проекте, в котором я реализовывал требования стандарта Web Content Accessibility Guidelines (WCAG). В нём был элемент размером 32 на 32 пикселей. Я установил его с помощью свойств width и height в единицах измерения rem.


.page__back-to-home {
  width: 2rem; /* 32px */
  height: 2rem; /* 32px */
}

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


В моём проекте для его вычисления мне нужно было из 56 пикселей вычесть 32 пикселя. Полученное значение (24 пикселей) поделить на 2. В итоге с каждой стороны родителя псевдоэлемент выйдет за его границы на 12 пикселей.


.page__back-to-home {
  --ha-clickable-area-expandable-ratio: 0.75rem; /* 12px */

  width: 2rem;
  height: 2rem; 
  /* на скриншоте значение свойств задаётся через --uia-control-icon-size */
}

Скриншот интерфейса. Есть ссылка назад в виде круга со стрелкой назад. У нее размер 24 на 24 пикселя. Кликабельная область 56 на 56 пикселя

Так лучше. Главное контролировать, чтобы области не накладывались друг на друга. Пользуйтесь на здоровье!


▍ Пользовательские свойства позволяют не раздувать код в медиа-запросах


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


.intro__heading { 
  font-size: 2rem; 
}

.intro__description { 
  font-size: 0.75rem; 
}

@media (min-width: 641px) {
  .intro__heading { 
    font-size: 3rem; 
  }

  .intro__description { 
    font-size: 1.25rem; 
  }
}

@media (min-width: 1025px) {
  .intro__heading { 
    font-size: 3.5rem; 
  }

  .intro__description { 
    font-size: 1.5rem; 
  }
}

Мы вынуждены создавать правила на каждое изменение свойства. По этой причине для изменения значения свойства font-size в каждом медиа-запросе по два правила с селектором .intro__heading и .intro__description. Этот стиль нельзя было изменить до появления пользовательских свойств. С их приходом у нас появился новый способ.


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


Применим данную технику на примере.


.intro__heading { 
  font-size: var(--heading-font-size, 2rem); 
}

.intro__description { 
  font-size: var(--hint-font-size, 0.75rem); 
}

@media (min-width: 641px) {
  .intro { 
    --heading-font-size: 3rem; 
    --hint-font-size: 1.25rem;
  }
}

@media (min-width: 1025px) {
  .intro { 
    --heading-font-size: 3.5rem; 
    --hint-font-size: 1.5rem;
  }  
}

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


▍ Свойство transform для центрирования элемента — это устаревший способ


Свойство transform долгое время было наиболее адекватным способом отцентрировать элемент, у которого установлено свойство position и значение absolute. Вся суть сводилась к двум шагам.


Первый — сдвинуть элемент на 50% от краёв элемента с помощью свойств top и left. Второй — с помощью значения translate(-50%, -50%) вернуть элемент обратно на половину своей ширины и высоты.


.parent {
  width: 20rem;
  height: 20rem;
  position: relative;
}

.parent::before {
  width: 2rem;
  height: 2rem;

  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

Сегодня у нас есть другой способ. Всё проще. Используем свойство place-items.


.parent {
  width: 20rem;
  height: 20rem;

  display: grid;
  place-items: center;
}

.parent::before {
  width: 2rem;
  height: 2rem;
  position: absolute;
}

Что тут происходит. Я убрал свойства top, left и transform. На их место добавил свойство place-items. Оно располагает элемент по центру. А ещё свойство position со значением relative больше тоже не нужно.


Вот так кода стало меньше. А его понятность возросла, поскольку нет скрытых штук.


▍ Заключение


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

  • способ вёрстки нестандартных чекбоксов и радиокнопок с помощью псевдоклассов :has() и :focus-within;
  • свойство inset как альтернативу свойствам top, right, bottom и left;
  • мой сниппет для расширения интерактивной области;
  • использование пользовательских свойств для сокращения кода в медиа-запросах;
  • свойство place-items, позволяющее без магии центрировать элемент со свойством position и значением absolute.

Спасибо за чтение!


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


Telegram-канал со скидками, розыгрышами призов и новостями IT ?

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


  1. Hallfire
    25.06.2024 16:00
    +2

    касательно place-items: center;

    Я убрал свойства top, left и transform. На их место добавил свойство place-items.

    а ещё было добавлено свойство display: grid;
    что весьма важно, т.к., насколько я понимаю, свойство place-items работает только с grid и flexbox (mdn)
    и из-за этого этот способ не всегда применим (без сопутствующих изменений других свойств/классов, что может быть весьма затратным)


  1. monochromer
    25.06.2024 16:00
    +4

    как избавиться от соседнего родственного комбинатора + при реализации нестандартных чекбоксов и радиокнопок

    Можно вообще без всяких обёрток и комбинаторов просто добавить appearance: none на нативный элемент и стилизовать почти как угодно, используя те же псевдоэлементы. К тому же такой способ позволяет использовать нативный вид фокуса.

    Пример: https://codepen.io/monochromer/pen/xxyMrqQ


    1. melnik909 Автор
      25.06.2024 16:00

      стилизовать почти как угодно, используя те же псевдоэлементы

      Скажите, пожалуйста, какие ограничения есть?


      1. dom1n1k
        25.06.2024 16:00

        Практически никаких значимых минусов. Уже года 3 минимум, как я подкатываю глаза к потолку, видя очередную статью про стилизацию чекбоксов через плюс :) А их (статей) до сих пор очень много, даже от сильных авторов, уж не знаю почему...


        1. melnik909 Автор
          25.06.2024 16:00
          +1

          Извините!). Для меня это было открытием. Мы тоже ошибаемся


      1. monochromer
        25.06.2024 16:00

        Скажите, пожалуйста, какие ограничения есть?

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


        1. melnik909 Автор
          25.06.2024 16:00

          Спасибо большое. Вы открыли для меня новое знание.


  1. enabokov
    25.06.2024 16:00
    +2

    Без картинок и плейграунда непонятно что происходит.


    1. zoto_ff
      25.06.2024 16:00
      +4

      как это у вас нет отрисовщика html+css прямо в голове?