
Хабр, привет! Я частенько пишу про работу CSS, его неизвестные возможности и влияние на доступность. Кажется, этих направлений мало для меня. Теперь я хочу показать техники вёрстки, используемые мной постоянно.
Цель — поделиться опытом с вами. Я использую не только трюки известных экспертов, есть лично мои придумки. Но, пожалуйста, относитесь к этому контенту, как просто альтернативному мнению. Мои техники не являются единственными правильными решениями.
Сегодня я расскажу:
- как избавиться от соседнего родственного комбинатора
+при реализации нестандартных чекбоксов и радиокнопок; - про свойство
inset, сокращающее код на целых три строки; - мой сниппет для расширения интерактивной области у кнопок и ссылок;
- стиль написания медиа-запросов, позволяющий сократить количество правил;
- альтернативный способ центрирования элемента без свойства
transform.
▍ Новый способ создания нестандартных чекбоксов и радиокнопок
Всю мою карьеру единственный способ создания нестандартных радиокнопок заключался в использовании селектора на основе соседнего родственного комбинатора +. Я уверен, что вы его знаете. А современные возможности CSS позволяют сделать по-другому.
В моей демонстрации будет использоваться следующая разметка:
<body> <div class="custom-radio-button"> <input id="rb-1" class="custom-radio-button__input sr-only" type="radio" name="radio" checked> <label for="rb-1" class="custom-radio-button__label">Вариант №1</label> </div> <div class="custom-radio-button"> <input id="rb-2" class="custom-radio-button__input sr-only" type="radio" name="radio"> <label for="rb-2" class="custom-radio-button__label">Вариант №2</label> </div> </body>
.sr-only { width: 1px; height: 1px; clip-path: inset(50%); overflow: hidden; position: absolute; white-space: nowrap; } .custom-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; } .custom-radio-button::before { content: ""; box-sizing: border-box; width: var(--custom-radio-button-size); height: var(--custom-radio-button-size); border: 1px solid #242424; border-radius: 100%; position: absolute; z-index: -1; } .custom-radio-button__label { display: grid; padding-left: calc(var(--custom-radio-button-dot-size) + var(--custom-radio-button-gap)); } .custom-radio-button__label::before, .custom-radio-button__label::after { content: ""; 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; } .custom-radio-button__label::before { border-top: var(--custom-radio-button-dot-size) solid #242424; border-left: var(--custom-radio-button-dot-size) solid #242424; transition: 0.3s; } .custom-radio-button__label::after { width: var(--custom-radio-button-dot-size); height: var(--custom-radio-button-dot-size); background-color: #2500e0; transition: 0.6s; }
Осталось сделать последний шаг. Дописать код для состояний, когда на радиокнопке сфокусировались и отметили её. Как я говорил ранее, он будет основан на соседнем родственном комбинаторе +.
.custom-radio-button__input:checked + .custom-radio-button__label::before { opacity: 1; scale: 1; } .custom-radio-button__input:focus + .custom-radio-button__label::after { scale: 3.6; opacity: 0.2; }
В чём проблема? Если в разметке элемент с классом .custom-radio-button__input случайно перестанет быть перед элементом .custom-radio-button__label, стилизация полетит к чёрту. В этом заключается проблема использования соседнего родственного комбинатора +.
В новой технике этого недостатка нет, потому что она основана на псевдоклассах :has() и :focus-within.
.custom-radio-button:has(:checked) .custom-radio-button__label::before { opacity: 1; scale: 1; } .custom-radio-button:focus-within .custom-radio-button__label::after { scale: 3.6; opacity: 0.2; }
Оба псевдокласса срабатывают на родителе, когда их потомок получает какое-либо состояние. В случае псевдокласса :has() у потомка применяются стили, когда срабатывает состояние «отмечено». А когда на потомке сфокусировались, то в дело включается псевдокласс :focus-within.
По сути можно обойтись одним псевдоклассом :has(), заменив псевдокласс :focus-within.
.custom-radio-button:has(:checked) .custom-radio-button__label::before { opacity: 1; scale: 1; } .custom-radio-button:has(:focus) .custom-radio-button__label::after { scale: 3.6; opacity: 0.2; }
Мне этот способ не нравится. Я любитель правил. Раз дали псевдокласс :focus-within, то надо его использовать. Но я не могу запрещать вам. Поэтому, если что, имейте в виду.
Спасибо monochromer. Оказывается, можно сделать всё проще. Если у элемента input установлен тип checkbox или radio, то у него есть псевдоэлементы ::before и ::after.
Следовательно код можно переписать с их участием
.custom-radio-button__input:checked::before { opacity: 1; scale: 1; } .custom-radio-button__input:focus::after { scale: 3.6; opacity: 0.2; }
.custom-radio-button { display: inline-flex; gap: 1rem; align-items: center; position: relative; isolation: isolate; } .custom-radio-button__input { appearance: none; margin: 0; width: 1rem; height: 1rem; border: 1px solid #242424; border-radius: 100%; display: grid; place-items: center; } .custom-radio-button__input::before, .custom-radio-button__input::after { content: ""; border-radius: 100%; opacity: 0; position: absolute; scale: 0; } .custom-radio-button__input::before { border: 0.25rem solid #242424; transition: 0.3s; } .custom-radio-button__input::after { width: 0.5rem; height: 0.5rem; background-color: #2500e0; transition: 0.6s; } .custom-radio-button__input:checked::before { opacity: 1; scale: 1; } .custom-radio-button__input:focus::after { scale: 3.6; opacity: 0.2; }
▍ Свойство 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, и псевдоэлементов::before()и::after; - свойство
insetкак альтернативу свойствамtop,right,bottomиleft; - мой сниппет для расширения интерактивной области;
- использование пользовательских свойств для сокр��щения кода в медиа-запросах;
- свойство
place-items, позволяющее без магии центрировать элемент со свойствомpositionи значениемabsolute.
Другие статьи из серии можно найти по тегу «sm909_css_tricks».
Спасибо за чтение!
P.S. Помогаю больше узнать про CSS в своём ТГ-канале CSS isn't magic. Присоединяйтесь. Ссылка в профиле.
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻

