В этом небольшом туториале я покажу вам, как с помощью нескольких новых возможностей CSS можно создать действительно прикольную динамичную систему тематизации (theming). Эта техника позволяет создавать тематизированные компоненты с тенями в светлом режиме и светящимися границами в темном режиме, с текстом, который всегда остается читаемым, независимо от динамичного цвета его фона. Все на CSS. Доступно во всех современных браузерах с мая 2026 года.
Вот что мы будем использовать:
@propertyдля регистрации значения кастомного свойства (custom property) с осмысленным синтаксисомlight-dark()- разрешает значения на основе вычисленной (computed)color-scheme(учитывающей настройки пользователей (user preferences))contrast-color()автоматически выбирает черный или белый текст для любого фона на основе алгоритма определения контраста WCAG@container style()- ветвление (branching) кастомных цветовых палитр с помощью запросов стилей (style queries) на основе того, во что разрешается (resolve)contrast-color()
И опциональная директива @function для того, чтобы сделать код чище (хотя она пока имеет ограниченную поддержку браузеров).
Шаг 1: light-dark() как основа

light-dark() - это, по сути, более простая альтернатива медиа-запросам настроек пользователей, касающихся цветовой темы. Функция принимает два значения цвета и возвращает первое в светлой цветовой схеме и второе в темной. Она “читает” из вычисленной color-scheme элемента. Это означает, что она реагирует как на prefers-color-scheme, так и на любое явное свойство color-scheme, установленное в CSS или с помощью JavaScript.
html { color-scheme: light dark; } body { /* Устанавливаем кастомное свойство --bg с помощью light-dark() */ --bg: light-dark(lightblue, black); background: var(--bg); }
Следует отметить одну вещь: light-dark() реагирует на вычисленную color-scheme, а не на медиа-запросы prefers-color-scheme. Установка color-scheme: light dark на корневом элементе (root) указывает браузеру учитывать настройки пользовательской ОС, но мы можем перезаписать схему путем явной установки color-scheme: dark (или light). Это означает, что мы можем перезаписать схему на любом уровне:
/* По умолчанию, учитываем настройки ОС */ html { color-scheme: light dark; } /* Переключаем раздел в темный режим, независимо от настроек ОС */ .dark-section { color-scheme: dark; }
Каждый вызов light-dark() внутри .dark-section - включая вызовы внутри @function --elevation() (см. ниже) - будут разрешаться в темное значение. В данном случае преимуществами по сравнению с медиа-запросами являются наследование и композиция.
Шаг 2: переключение механизма выделения
В светлых темах карточки выглядят объемными благодаря теням. Но в темных темах тени растворяются в фоне… поэтому нам нужен другой механизм выделения (elevation - дословно, подъем). Мы создадим едва заметное неоновое свечение краев темных поверхностей.
light-dark() принимает только значения <color>, поэтому ей нельзя передать определения теней целиком. Вместо этого, можно использовать light-dark() для цветов слоев теней, чтобы сделать нежелательные слои transparent (прозрачными) в неправильном режиме:
.card { box-shadow: /* Небольшая видимая тень в светлом режиме, прозрачная в темном */ 0 1px 2px light-dark(lightgray, transparent), 0 4px 12px light-dark(lightgray, transparent), 0 12px 32px light-dark(lightgray, transparent), /* Светящаяся прозрачная граница в светлом режиме, видимая в темном */ inset 0 0 0 1px light-dark(transparent, oklch(from var(--bg) calc(l + 0.5) c h / 0.5)), 0 0 16px light-dark(transparent, oklch(from var(--bg) calc(l + 0.5) c h / 0.3)), 0 0 3px light-dark(transparent, oklch(from var(--bg) calc(l + 0.5) c h / 0.5)); }
Все 6 слоев теней технически всегда присутствуют, но 3 из них являются transparent в любом режиме.
Кроме того, для светящейся границы синтаксис относительного цвета (oklch(from var(--bg) ...)) генерирует (derive) цвета свечения из фона карточки, поэтому выделение всегда ощущается привязанным к карточке.
Это работает, но код придется повторять для каждого выделяемого элемента.
@functionпозволяет сделать код чище:
@function --elevation(--bg) { result: 0 1px 2px light-dark(lightgray, transparent), 0 4px 12px light-dark(lightgray, transparent), 0 12px 32px light-dark(lightgray, transparent), inset 0 0 0 1px light-dark(transparent, oklch(from var(--bg) calc(l + 0.5) c h / 0.5)), 0 0 16px light-dark(transparent, oklch(from var(--bg) calc(l + 0.5) c h / 0.3)), 0 0 3px light-dark(transparent, oklch(from var(--bg) calc(l + 0.5) c h / 0.5)); }
Затем достаточно просто применить эту функцию к нужному элементу:
.card { box-shadow: --elevation(var(--bg)); }
Обратите внимание, что
@functionпока имеет ограниченную поддержку браузеров.

Шаг 3: автоматический контраст с помощью contrast-color()

Я писала о contrast-color() несколько месяцев назад. Это новая функция, принимающая любой цвет и возвращающая black или white в зависимости от того, какой цвет больше контрастирует с переданным. Мы используем ее для обеспечения читаемости текста, независимо от цвета бренда:
.card { --bg: var(--brand-color); background: var(--bg); color: contrast-color(var(--bg)); }
Вот и все. Одна строчка для доступного (accessible) текста. Но что если мы не хотим ограничиваться черным и белым?
Шаг 4: кастомизируемые палитры с помощью запросов стилей

Запросы стилей (style queries) позволяют использовать contrast-color() как индикатор, т.е. ответить на вопрос: “На светлой или темной поверхности находится элемент?”, и затем выбрать более богатую цветовую палитру. Я писала об этой технике в статье contrast-color() beyond black & white.
Сначала регистрируем кастомное свойство и присваиваем ему результат contrast-color():
@property --contrast-color { syntax: "<color>"; initial-value: white; inherits: true; } .card { --bg: var(--brand-color); --contrast-color: contrast-color(var(--bg)); color: var(--contrast-color); }
Затем используем @container style() для ветвления:
/* Если contrast-color имеет значение white, фон темный: используем более теплые светлые цвета */ @container style(--contrast-color: white) { .card-label, .card-body { /* Относительная цветовая палитра для основного текста и текста подписей */ color: oklch(from var(--bg) 0.9 0.1 h); } } /* Если contrast-color имеет значение black, фон светлый: используем глубокие тона на основе оттенка карточки */ @container style(--contrast-color: black) { .theming-card-label, .theming-card-body { color: oklch(from var(--bg) 0.3 0.1 h); } }
Вместо чисто черного или белого цвета, текст окрашивается в соответствии с оттенком карточки - глубокий индиго на светло-лавандовом фоне, теплый кремовый на темно-бирюзовом. Синтаксис относительного цвета (oklch(from var(--bg) ...)) определяет оттенок на основе одного цвета бренда.
Вместо синтаксиса относительного цвета, можно использовать явные значения цвета. Однако в этом случае нельзя полагаться на результат color-contrast() напрямую, поэтому следует тщательно тестировать используемые цветовые комбинации.
Все вместе
Собрав все вместе, можно отделить макро-тематизацию (светлую и темную темы всей страницы, установленные с помощью light-dark()) от микро-тематизации (темы внутри карточки/элемента, использующего преимущества contrast-color()).
Например, у нас может быть темная тема страницы, к которой применяются определенные стили выделения, но при этом у нас также может быть карточка с более светлым или темным фоном. В этом случае мы получим более надежный результат и более динамичные варианты цветовых решений, используя contrast-color() и другие методы, упомянутые в статье.
Вот полное демо - переключайте тему, чтобы увидеть смену теней на светящиеся границы, и заметьте, как цвет текста каждой карточки меняется на основе ее поверхности:
Каждая карточка получает только --brand-color (сама с помощью light-dark() для соответствующих теме значений). Далее система генерирует:
фон - сам цвет бренда
механизм выделения - серые тени или светящаяся граница на основе фона
цвет текста -
contrast-color()берет черный или белый, а запросы стилей выполняют ветвление в нужную цветовую палитру
Поддержка браузеров
Как упоминалось в начале статьи, эта техника поддерживается всеми основными браузерами с мая 2026 года.
@function пока поддерживается только в Chrome 139+.
Заключение
Сочетание light-dark(), @container style() и contrast-color() создает систему тематизации, которая является декларативной, компонуемой и использующей преимущества разных уровней цветовой тематизации. Она учитывает настройки пользователей (через цветовую схему) для макро-тематизации и позволяет создавать динамичные микро-темы.
Материалы для дополнительного изучения:
