Магия CSS на практике: советы по вёрстке от гика

в 9:00, , рубрики: css, ruvds_статьи, верстка, лайфхаки, псевдоклассы, псевдоэлементы, радиокнопки, центрирование, чекбоксы
Магия CSS на практике: советы по вёрстке от гика - 1

Я частенько пишу про работу 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 💻

Автор: Стас Мельников

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js