Новые и старые возможности CSS

В CSS появляются новые возможности, которые позволяют создавать более сложные стили и логику прямо внутри самого CSS. Кажется, что скоро препроцессоры, такие как Sass, Less и другие решения, будут не особо-то и нужны.
Рассмотрим подробнее операторы and, or, xor, not в контексте CSS и другие возможности такие как if, @function, color-mix, @scope и @layer.
Новая функция if()
Это самая ожидаемая функция, которая внедряется в спецификацию CSS. Она позволяет менять значение свойства в зависимости от условия прямо внутри строки.
Важно: На данный момент работает только в Chrome Canary c флагом «Experimental Web Platform features». - https://caniuse.com/?search=if
Синтаксис: if(условие, значение-если-true, значение-если-false)
Пример (Inline If):
.container {
/* Если переменная --variant равна 'dark', фон черный, иначе белый */
background-color: if(style(--variant: dark), black, white);
/* Вложенность тоже планируется */
padding: if(style(--size: large), 20px, if(style(--size: medium), 10px, 5px));
}
Логика в медиа-запросах (@media, @supports)
Здесь логические операторы используются давно и поддерживаются всеми браузерами.
AND (И)
Используется ключевое слово and. Оба условия должны быть истинными.
/* Экран шире 600px И ориентация альбомная */
@media (min-width: 600px) and (orientation: landscape) {
body { background: lightblue; }
}
OR (ИЛИ)
Используется ключевое слово or (в новых спецификациях) или запятая , (классический способ).
/* Устройство поддерживает наведение ИЛИ ширина больше 1000px */
@media (hover: hover), (min-width: 1000px) {
.btn { display: block; }
}
NOT (НЕ)
Инвертирует условие.
/* Применить стили везде, КРОМЕ цветных экранов */
@media not all and (color) {
body { border: 1px solid black; }
}
Логика в селекторах
CSS позволяет строить логику выбора элементов.
NOT (Отрицание)
Псевдокласс :not().
/* Все кнопки, у которых НЕТ класса .disabled */
button:not(.disabled) {
cursor: pointer;
}
OR (ИЛИ)
Псевдоклассы :is() и :where(). Они выбирают элемент, если он соответствует хотя бы одному селектору из списка.
/* Выбрать элемент, если это h1 ИЛИ h2 ИЛИ h3 */
:is(h1, h2, h3) {
font-weight: bold;
}
AND (И)
В селекторах нет слова and. Логическое «И» достигается записью селекторов без пробела (цепочкой).
/* Элемент имеет класс .box И класс .active */
.box.active {
border-color: red;
}
Сложная логика: XOR (Исключающее ИЛИ)
Стоит заметить, что в CSS нет встроенного оператора XOR. Логика XOR: Истинно, если выполняется только одно из условий, но не оба сразу.
Как реализовать? Приходится комбинировать
notиand.
Пример XOR в селекторах: Допустим, нам нужно стилизовать элемент, если у него есть класс .a ИЛИ класс .b, но не оба сразу.
/* (A и не B) ИЛИ (B и не A) */
:is(.a:not(.b), .b:not(.a)) {
color: purple;
}
Трюк «Space Toggle» (Хак для IF/ELSE)
Пока функция if() не стала стандартом, разработчики используют трюк с CSS-переменными для создания переключателей.
Суть: CSS-переменная может иметь значение initial (пустота, ломающая свойство) или пробел (валидное значение).
:root {
/* Переключатели */
--ON: initial;
--OFF: ;
}
.card {
/* Настройка: включить темную тему */
--is-dark: var(--ON);
/* Логика:
Если --is-dark = initial (ON), сработает фолбек (black).
Если --is-dark = " " (OFF), сработает первое значение (white).
*/
background-color: var(--is-dark, white) var(--is-dark, black); /* Не работает напрямую так просто, нужен сложный var() стек */
}
@function
Это абсолютно новая (экспериментальная) возможность, которая только появляется в стандартах CSS.
Это часть той же новой спецификации, что и if(). Теперь мы сможем писать функции, которые работают прямо в браузере.
Главное отличие от SCSS: они динамические, могут пересчитываться при изменении переменных, изменении размеров экрана и т.д.
Синтаксис: Имя функции должно начинаться с дефисов (как переменная), например --my-func.
/* Объявление функции */
@function --negate(--value) {
result: calc(-1 * var(--value));
}
/* Использование */
.box {
margin-left: --negate(20px); /* Результат: -20px */
}
и еще пример:
@function --lighten-if-dark(--color type(<color>), --amount type(<percentage>)) {
/* Используем if() внутри функции! */
result: if(
media(prefers-color-scheme: dark),
color-mix(in srgb, var(--color), white var(--amount)),
var(--color)
);
}
.button {
/* Если тема темная, цвет станет светлее на 20%, иначе останется синим */
background: --lighten-if-dark(blue, 20%);
}
Поддержка экспериментальная https://caniuse.com/?search=%40function
color-mix
color-mix() - это очень мощная нативная CSS-функция, которая позволяет смешивать два цвета в определённой пропорции прямо в браузере.
Это «убийца» многих функций Sass (типа darken(), lighten(), mix()), потому что она работает с CSS-переменными в реальном времени.
Синтаксис:
color-mix(in <цветовое-пространство>, <цвет-1> <процент>?, <цвет-2> <процент>?)
Обязательный параметр - цветовое пространство (обычно srgb или oklch).
/* Простой пример: смешать красный и синий пополам */
.box {
background: color-mix(in srgb, red, blue); /* Результат: фиолетовый */
}
Главные сценарии использования
А. Прозрачность для переменных (Killer Feature)
Раньше, если у вас был цвет в hex-переменной (--primary: #ff0000), вы не могли просто добавить к нему прозрачность. Приходилось разбивать его на R, G, B каналы. Теперь это делается в одну строку:
:root {
--primary: #ff0000;
}
.overlay {
/* Смешиваем цвет с "прозрачным" цветом на 50% */
background: color-mix(in srgb, var(--primary), transparent 50%);
/* Это аналог rgba(#ff0000, 0.5), но работает с любой переменной! */
}
Б. Автоматическая генерация палитры (Hover/Active)
Вместо того чтобы вручную подбирать цвета для состояний кнопки, можно высчитывать их автоматически.
.btn {
background-color: var(--color);
}
.btn:hover {
/* При наведении смешиваем с белым (осветляем на 10%) */
background-color: color-mix(in srgb, var(--color), white 10%);
}
.btn:active {
/* При клике смешиваем с черным (затемняем на 10%) */
background-color: color-mix(in srgb, var(--color), black 10%);
}
Почему это круто: Вы меняете только одну переменную --color, а все состояния (:hover, :active) пересчитываются сами.
В. Создание оттенков (Theming)
Можно создать всю тему сайта, задав только один акцентный цвет.
:root {
--brand: #3498db;
/* Слабый фон (смесь с белым 90%) */
--brand-light: color-mix(in srgb, var(--brand), white 90%);
/* Темный текст (смесь с черным 60%) */
--brand-dark: color-mix(in srgb, var(--brand), black 60%);
}
Хорошая поддержка браузерами - https://caniuse.com/?search=color-mix
@Layer
@layer (каскадные слои) - это революция в управлении весом (специфичностью) селекторов. Это инструмент, который позволяет вам сказать браузеру: «Мне неважно, насколько "жирный" селектор написан в той библиотеке, мои стили в слое theme должны быть главнее».
Но какую проблему мы решаем? Раньше, чтобы перебить стили какой-нибудь библиотеки (например, Bootstrap) или старого легаси-кода, нам приходилось:
- Писать длинные цепочки селекторов: body .container .card button { ... }.
- Использовать
!important(что плохо). - Следить за порядком подключения файлов.
С @layer вы просто создаете слои приоритетов.
Синтаксис
Есть несколько способов создать слой:
Блоком
@layer base {
body { margin: 0; }
}
При импорте (очень удобно для сторонних библиотек)
/* Весь Bootstrap уйдет в слой 'vendor' и не будет мешать вашим стилям */
@import url('bootstrap.css') layer(vendor);
Best Practice: Лучше всего в самом начале CSS-файла объявить порядок слоев, чтобы потом не путаться.
@layer reset, lib, theme, components, utilities;
Как это работает
Представьте, что вы раскладываете стили по папкам (слоям). Порядок слоев важнее, чем вес селекторов внутри них.
В обычном CSS селектор по #id всегда побеждает селектор по .классу. Но не с @layer!
/* Объявляем порядок слоев: от слабого к сильному */
@layer framework, custom;
/* Слой framework (слабый приоритет) */
@layer framework {
#app {
background: blue; /* У этого селектора огромный вес (ID) */
}
}
/* Слой custom (сильный приоритет) */
@layer custom {
.box {
background: red; /* У этого селектора маленький вес (класс) */
}
}
/* HTML: <div id="app" class="box"></div> */
/* ИТОГ: Фон будет КРАСНЫМ. */
Важный нюанс: Стили без слоя (Unlayered). Это самая частая ошибка новичков. Стили, написанные просто так (вне любого
@layer), всегда имеют наивысший приоритет.
@layer my-layer {
p { color: red !important; }
}
/* Этот стиль НЕ в слое */
p { color: blue; }
/* Итог: текст будет СИНИМ (если не использовать !important в слое,
но даже !important в слоях работает хитро).
Обычные стили считаются "финальным слоем". */
@layer поддерживается во всех современных браузерах (Chrome, Firefox, Safari, Edge) уже пару лет. Это не экспериментальная функция, ей можно смело пользоваться. Подробнее тут https://caniuse.com/?search=%40layer
@scope
@scope - это долгожданная возможность нативной изоляции стилей. Если @layer управляет «весом» (приоритетом) стилей, то @scope управляет их областью действия (географией в DOM).
Главная задача @scope:
- Сделать так, чтобы стили применялись только к определённому куску HTML (компоненту).
- Сделать так, чтобы стили не «протекали» глубже, чем нужно.
Это нативная замена методологиям вроде BEM (.block__element) и инструментам вроде CSS Modules (Scoped CSS), но работающая прямо в браузере.
Базовый синтаксис
Мы задаем корень (root), внутри которого работают стили.
/* Стили применятся ТОЛЬКО внутри элемента с классом .card */
@scope (.card) {
/* Этот img выберется, только если он внутри .card */
img {
border-radius: 10px;
}
/* :scope — это ссылка на сам корень (.card) */
:scope {
background: white;
padding: 20px;
}
}
В чем отличие от вложенности (Nesting) .card img { ... }? Вложенность увеличивает вес селектора (специфичность). @scope - нет. Это позволяет легче переопределять стили.
Главная фишка: «Пончик» (Donut Scoping)
Представьте, что у вас есть компонент .card, а внутри него есть зона .content, куда вставляется произвольный текст (например, из CMS). Вы хотите стилизовать каркас карточки, но не хотите, чтобы эти стили влияли на контент внутри .content.
Это называется «вырез в области видимости» (нижняя граница).
Синтаксис: @scope (отсюда) to (досюда)
/* Применять стили ВНУТРИ .card, НО НЕ ВНУТРИ .content */
@scope (.card) to (.content) {
p {
color: gray; /* Покрасит параграфы карточки */
}
/* Если внутри .content будет <p>, этот стиль к нему НЕ применится!
Браузер "остановится" на границе .content */
}
Это решает вечную боль: «Почему мои глобальные стили для .title ломают заголовок внутри виджета комментариев?». С @scope вы просто исключаете внутренности виджета.
Принцип близость (Proximity)
@scope меняет то, как браузер решает конфликты стилей. В обычном CSS побеждает более "тяжелый селектор". В Scoped CSS при прочих равных побеждает тот, кто ближе в DOM-дереве.
Пример: У нас есть две темы: темная (глобальная) и светлая (локальная внутри темной).
<div class="dark-theme">
<a href="#">Я ссылка глобальной темы</a>
<div class="light-theme">
<a href="#">Я ссылка локальной темы</a>
</div>
</div>
@scope (.dark-theme) {
a { color: white; }
}
@scope (.light-theme) {
a { color: black; }
}
Даже если селекторы одинакового веса, ссылка внутри .light-theme будет черной, потому что скоуп .light-theme к ней геометрически ближе в HTML, чем .dark-theme. Раньше для этого приходилось писать .dark-theme .light-theme a { ... }.
Inline-стили нового поколения
@scope можно использовать прямо внутри HTML в теге <style>. Это супер-удобно для Vue/React/Svelte, если бы они перешли на нативные механизмы.
<div class="my-component">
<style>
@scope {
:scope { color: red; }
span { font-weight: bold; }
}
</style>
<span>Я жирный и красный</span>
</div>
<div>
<span>Я обычный, стили выше меня не касаются</span>
</div>
С поддержка не все так хорошо - https://caniuse.com/?search=%40scope
На этом всё. Надеюсь, эта статья была вам полезна. ❤️