Я частенько пишу про работу 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 */
}
Так лучше. Главное контролировать, чтобы области не накладывались друг на друга. Пользуйтесь на здоровье!
▍ Пользовательские свойства позволяют не раздувать код в медиа-запросах
При вёрстке любого проекта приходится писать кучу правил внутри медиа-запросов. От таких размеров кода легко потеряться в нём. В качестве примера проблемы, о которой идёт речь, я напишу стили для двух элементов с классами .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. Присоединяйтесь. Ссылка в профиле.
Автор: Стас Мельников