Современные возможности CSS
Что это такое
За последние несколько лет в CSS появилась горстка возможностей, заменяющих работу, которую раньше делали препроцессоры, JavaScript или груда классов. Это пользовательские свойства для значений, которым можно дать имя и менять их, вложенность для читаемой области видимости, логические свойства для отступов, не зависящих от направления, семейство :has() для выбора с учётом родителя, а также функции вроде clamp() и color-mix() для значений, которые адаптируются сами.
Почему это важно
Каждая из них удаляет из среднего проекта целую категорию сложности. Пользовательские свойства вернули темизацию времени выполнения в разряд вопросов стилей, а не сборки, и убрали причину тянуться к библиотеке CSS-in-JS. Вложенность убрала причину тянуться к Sass. Логические свойства убрали целый класс ошибок «мы забыли про языки с письмом справа налево». Однако нужны вам не все из них, и не стоит применять возможность, пока вы не можете объяснить, что она заменяет. Знание того, что они есть, всё же меняет проект таблицы стилей, которую вы собираетесь написать.
Ключевые идеи
Пользовательские свойства — это значения, а не переменные
Пользовательское свойство наследуется, поэтому задание его на поддереве переопределяет тему всего внутри без нового селектора для каждого элемента. Оно также участвует в каскаде, и именно это делает темизацию на уровне компонента рабочей: потребитель задаёт свойство, компонент читает его со значением по умолчанию.
.button {
--button-bg: navy;
background: var(--button-bg);
color: white;
}
.card call-to-action .button { --button-bg: teal; }
Поскольку значение хранится как текст до момента использования, оно может держать длину, цвет или целый box-shadow; как это истолковать, решает компонент.
Вложенность держит селекторы рядом с их контекстом
Вложенные правила используют & для родителя, а в остальном это обычный CSS, который компилирует браузер, а не шаг сборки. Держите вложенность неглубокой: три уровня & воссоздают те самые проблемы специфичности, которых вложенность должна была избежать.
.card {
padding: 1rem;
& > h3 { margin-block-start: 0; }
&:hover { border-color: teal; }
}
Логические свойства называют роль, а не сторону
margin-block, padding-inline, border-start-start-radius и подобные отображаются на физические стороны согласно направлению письма. Использование их вместо left/right значит, что язык с письмом справа налево или вертикальным письмом не требует отдельной таблицы стилей.
:is(), :where() и :has() меняют то, что может выразить селектор
:is() группирует альтернативы, не повторяя остаток селектора, :where() работает так же, но вносит нулевую специфичность, а :has() позволяет селектору заглянуть в потомка — пустое поле формы, карточка с изображением внутри, — что раньше требовало JavaScript.
.field:has(input:invalid) { border-color: darkred; }
:where(ul, ol) > li { margin-block: 0.25rem; }
Функции собирают значения до их использования
clamp(min, ideal, max) даёт плавный шрифт или отступ одной строкой, min() и max() выбирают между значениями, а color-mix() выводит цвет из двух других, так что палитра остаётся согласованной при изменении одного из них.
h1 { font-size: clamp(1.5rem, 2vw + 1rem, 3rem); }
.hint { color: color-mix(in oklab, navy 60%, white); }
Практика
Замените медиа-запрос на плавное значение
Найдите правило, которое прыгает между двумя размерами шрифта на точке перелома. Замените его выражением clamp(), которое интерполирует между ними.
Готово, когда: текст масштабируется плавно по мере сужения окна и остаётся в пределах двух исходных размеров, а медиа-запрос удалён, а не просто обойдён.
Темизируйте компонент без новых селекторов
Возьмите компонент-кнопку и управляйте её фоном, padding и радиусом через пользовательские свойства. Задайте двум экземплярам разные темы, устанавливая свойства только на самих экземплярах.
Готово, когда: ни у одного экземпляра нет отдельного селектора, и изменение одного свойства на экземпляре меняет только это свойство.
Частые ошибки
- Пользовательское свойство без значения по умолчанию — незаданная переменная делает объявление недействительным, и свойство откатывается к начальному значению без ошибки; исправить: давайте
var(--name, fallback)везде, где свойство необязательно. - Вложенность на три-четыре уровня — скомпилированные селекторы набирают специфичность и становятся трудно переопределяемыми; исправить: вкладывайте на один уровень, а когда нужно два — берите класс.
- Смешивание физических и логических свойств в одном компоненте — раскладка верна в одном направлении и сломана в другом; исправить: выберите логические свойства для всех отступов и границ в этом компоненте.
- Убеждение, что
:has()бесплатен — он заставляет браузер перепроверять потомков при изменении дерева; исправить: держите его на небольших устойчивых структурах, а не на селекторе по всей странице. - Новая возможность без слоя запасных значений — старый браузер молча отбрасывает объявление; исправить: поставьте простое значение первым, а современное — после него.
Что читать дальше
- Медиа- и контейнерные запросы (02-frontend/05-css/03-media-and-container-queries) — контейнерные запросы и современные функции обычно применяют вместе.
- Дизайн-токены (03-design-ux/41-design-tokens) — пользовательские свойства — естественная реализация слоя токенов.
- Tailwind CSS (02-frontend/14-tailwind-css) — альтернативный ответ на многие из этих же задач, с другими компромиссами.