Статья поделена на две части. Первой части будет достаточно, чтобы понять как применять новые кастомные медиавыражения в проекте. Вторая часть, для любопытных, которые хотят понять как это работает под капотом и почему кастомные свойства не работают в медиавыражениях.
Часть 1: Введение
Впервые фронтенд-разработчик понимает, что конструкция типа --variable: value в CSS - это не переменная, когда пытается использовать её в медиавыражении:
:root {
--tablet-width: 768px;
}
@media (min-width: var(--tablet-width)) {
.text {
font-size: 24px;
}
}
или так:
:root {
--tablet-width: (min-width: 768px);
}
@media (var(--tablet-width)) {
.text {
font-size: 24px;
}
}
В этот самый момент срочно приходится искать как работают кастомные свойства и выяснять почему они работают не так как ожидается, или вообще не работают. Во второй части статьи я объясню этот момент.
С тех самых пор как появились кастомные свойства, W3C начали думать, как сделать так, чтобы кастомки заработали в медиавыражениях. Короткий ответ: никак! Нужно изобретать новую директиву — @custom-media.
Обычные кастомные свойства не умеют подставляться внутрь @media, потому что там требуется логическая структура, а не текстовая замена. Поэтому в спецификации появилась совершенно отдельная штука — правило @custom-media, которое позволяет объявлять именованные алиасы для медиавыражений. Это не подстановка текста, а именно логическое вычисление.
Все наработки по использованию кастомок в медиавыражениях вылились в спецификацию Media Queries Level 5, а уже 9 декабря 2025 года, за флагом layout.css.custom-media.enabled их можно будет попробовать в 146 версии Firefox.
Ежедневное использование медиавыражений:
Давайте для начала вспомним ежедневное использование медиавыражений в дикой природе.
.text {
font-size: 20px;
}
@media (min-width: 768px) {
.text {
font-size: 24px;
}
}
Кто-то уже позволяет себе в проде использовать медиафичу range:
.text {
font-size: 20px;
}
/* аналогично (min-width: 768px) */
@media (width >= 768px) {
.text {
font-size: 24px;
}
}
А кто-то даже CSS Nesting использует:
.text {
font-size: 20px;
/* директива внутри селектора */
@media (width >= 768px) {
font-size: 24px;
}
}
Ну и давайте посмотрим на использование переменных в медиавыражениях в препроцессорах, чтобы быстрее понять кастомные медиавыражения:
$tablet-width: 768px;
.text {
font-size: 20px;
// в препроцессоре переменную можно использовать как значение
// даже в медиавыражениях
@media (min-width: $tablet-width) {
font-size: 24px;
}
}
Проблема
Основная задача, для которой используются переменные - это переиспользование человекопонятных значений. Один раз определив в проекте значение переменной и её можно использовать в разных местах, причём часто даже не зная конкретного значение этой переменной.
$mobile-width: 320px;
$tablet-width: 768px;
$desktop-width: 1024px;
Использование переменных для брейкпоинтов для фронтенд-разработчика - это уже что-то очень интуитивно понятное. Фронтендер уже не обращает внимание на значения, он смотрит и по памяти использует название переменной.
@media (min-width: $desktop-width){}
сразу же говорит для какого экрана будут изменения. Такого же процесса работы в CSS можно добиться с помощью кастоных медиавыражений.
Кастомные медиавыражения
Кастомные медиавыражения включают в себя лучшее из двух миров: переменных в препроцессарах и кастомных свойств в CSS.
Создадим первое кастомное медиавыражение:
@custom-media --mobile-width (min-width: 320px);
Синтаксис
Синтаксис кастомного медиавыражения:
@custom-media <extension-name> [ <media-query-list> | true | false ];
1) @custom-media - это ключевое слово — директива, которая объявляет новое кастомное медиавыражение (alias). По смыслу похоже на @font-face, @property, @media, но здесь вы создаёте новое имя, которое потом сможете использовать в медиавыражениях.
2) <extension-name> - имя кастомного медиавыражения.
Как выглядит:
должно начинаться с
--;может содержать буквы, цифры, дефисы.
Примеры корректных имён:
--narrow-window
--modern
--supports-pen
--retina-only
Эти имена затем используются в @media:
@media (--narrow-window) {}
3) [ <media-query-list> | true | false ] - Это значение alias-а. Оно может быть одним из трёх вариантов:
- <media-query-list> - список медиавыражений. Фактически обычный контент, который стоит после @media.
- true - Это объявление кастомного медиавыражения, которое всегда истинно.
@custom-media --always-true true;
@media (--always-true) {
/* Всегда сработает */
}
- false - Это объявление медиавыражения, которое всегда ложно.
Примеры
@custom-media --modern (hover), (color);
Расшифровка:
- @custom-media — объявляем кастомное медиавыражение
- --modern — имя alias-а
- (hover), (color) — логическое выражение: true, если хотя бы одно истинно
Использование:
@media (--modern) and (width > 900px) {
/* стили для современных устройств */
}
Второй пример
В препроцессоре обычно создаётся отдельный файл для работы с брейкпоинтами:
/* breakpoints.scss */
$mobile: 480px;
$tablet: 768px;
$laptop: 1024px;
$desktop: 1280px;
$wide: 1536px;
в кастомных медиавыражения этот пример будет выглядеть вот так:
@custom-media --mobile (width >= 480px);
@custom-media --tablet (width >= 768px);
@custom-media --laptop (width >= 1024px);
@custom-media --desktop (width >= 1280px);
@custom-media --wide (width >= 1536px);
Использование:
@media (--tablet) {
.sidebar {
display: block;
}
}
Как видите, любой, кто уже поработал с переменными в медиавыражениях в препроцессорах достаточно быстро разберётся с @custom-media в CSS.
Хочу использовать прямо сейчас
Поскольку кастомные медиавыражения довольно просто превратить в обычные медиавыражения достаточно быстро появился плагин для postcss postcss-custom-media, который по сути берёт значение @custom-media и подставляет его вместо <media-query-list>
@custom-media --mobile (min-width >= 480px);
@media (--mobile) {
.text {
font-size: 24px;
}
}
в итоговый CSS генерируется
@media (width >= 480px) {
.text {
font-size: 24px;
}
}
На этом первая часть закончена. В целом, вы уже можете пробовать новые ещё даже не вышедшие кастомные медиавыражения, но пока только с плагинами.
Вторая часть для любознательных
Что там с @custom-media и почему нельзя было сразу сделать нормально, чтобы использовать кастомные свойства(--mobile-width) в медиавыражениях?
Чтобы полностью разобраться в процессе того как браузер обрабатывает значения нужно прочесть десятки спецификаций, узнать о всех этапах обработки:
фильтрация;
каскад;
значение по умолчанию;
различия вычисленных, используемых и действительных значений;
типы и их особенности вычисления
и многое другое. Но ты читатель тут конечно же не за этим. Я расскажу короткую историю, которой будет достаточно для оправдания W3C и понятной нам с вами.
В коротком варианте, достаточным для понимания, работа браузера по вычислению значений в старом стиле выглядит так:
Получение CSS-файла
Парсинг CSS
Этап правил. Здесь читаются обычные медиавыражения
Каскад + наследование
Вычисление значений. Вот тут разрешаются
var()и кастомные свойства. На этом этапе медиа уже давно обработаны (их невозможно пересчитать черезvar()).
Коротким ответом на изначальный вопрос станет: медиавыражения вычисляются гораздо раньше, чем кастомные выражения. Именно поэтому медиавыражения не понимают кастомные свойства, так как они ещё не вычислились
:root {
--tablet-width: 768px;
}
/*CSS: я не понимаю чему равно значение */
@media (min-width: var(--tablet-width)) {
.text {
font-size: 24px;
}
}
Решением же стало добавление кастомных медиавыражений, которые вычисляются до медиавыражений:
Получение CSS-файла
Парсинг CSS
Сбор
@custom-mediaСоздаётся таблица алиасов--mobile->min-width: 768pxЭтап правил. Сначала раскрываются
custom-media:@media(--mobile)->media(min-width: 320px. Затем@mediaвычисляются: еслиmin-width: 768px→true, правило активно, еслиfalse→ правило игнорируетсяКаскад + наследование
Вычисление значений. Вот тут разрешаются
var()и кастомные свойства.
На это всё. Спасибо, что прочитали.