Уроки

CSS-функция light-dark(): нативное переключение тем

7 мин чтения

Реализация тёмного режима исторически требовала немалого шаблонного кода: медиазапрос prefers-color-scheme, переопределяющий каждую кастомную переменную, JavaScript для пользовательского переключения, localStorage для сохранения предпочтений и встроенный скрипт в <head> для предотвращения мигания неверной темы при загрузке страницы. CSS-функция light-dark() не устраняет всё это, но значительно сокращает объём CSS для решения этой задачи.

light-dark() — CSS-функция для работы с цветом, которая принимает ровно два значения и возвращает первое, когда активная цветовая схема светлая, или второе, когда тёмная. Это семантический CSS-эквивалент тернарного оператора для цветов.

Что такое light-dark()?

Сигнатура функции проста:

color: light-dark(<light-color>, <dark-color>);

Когда активная цветовая схема светлая, браузер использует <light-color>. Когда тёмная — <dark-color>. «Активная цветовая схема» определяется CSS-свойством color-scheme, которое, в свою очередь, реагирует на системный медиазапрос prefers-color-scheme или явное значение, заданное для элемента.

Функция поддерживается в:

  • Chrome/Edge: с версии 123 (март 2024)
  • Firefox: с версии 120 (ноябрь 2023)
  • Safari: с версии 17.5 (июнь 2024)

Глобальная поддержка составляет около 85% по состоянию на начало 2026 года. Это относительно новое дополнение, но покрытие браузерами растёт достаточно быстро для использования в production с резервной стратегией.

Как это работает со свойством color-scheme

light-dark() не работает изолированно. Она полностью зависит от правильной установки CSS-свойства color-scheme. Без него функция не имеет контекста для определения возвращаемого значения.

Свойство color-scheme объявляет, какие цветовые схемы поддерживает документ или элемент. Установка его на :root — отправная точка:

:root {
  color-scheme: light dark;
}

Это единственное объявление сообщает браузеру, что ваша страница поддерживает обе схемы. Затем браузер:

  1. Считывает системное предпочтение пользователя prefers-color-scheme
  2. Применяет соответствующую схему
  3. Заставляет все вызовы light-dark() на странице разрешаться в подходящее значение

После этого определение цветов, адаптированных к теме, сводится к написанию одиночных объявлений:

:root {
  color-scheme: light dark;

  --color-background: light-dark(#FFFFFF, #0F0F17);
  --color-text:       light-dark(#1A1A2E, #E8E8F0);
  --color-border:     light-dark(#DEE2E6, #2E2E4A);
  --color-accent:     light-dark(#2563EB, #60A5FA);
}

Никакого медиазапроса. Никаких переопределений в селекторах. Одно объявление на цвет, оба значения в одной строке. Браузер обрабатывает переключение автоматически на основе системных предпочтений.

Ограничение одной схемой

Установка color-scheme: light или color-scheme: dark принудительно задаёт одну схему независимо от системных предпочтений:

/* Всегда светлый, независимо от настроек ОС */
.widget {
  color-scheme: light;
  background: light-dark(#FFFFFF, #0F0F17);
  /* Всегда разрешается в #FFFFFF */
}

/* Всегда тёмный */
.dark-panel {
  color-scheme: dark;
  color: light-dark(#1A1A2E, #E8E8F0);
  /* Всегда разрешается в #E8E8F0 */
}

Это полезно для компонентов интерфейса, которые всегда должны отображаться в определённом режиме — например, редактор кода, который всегда должен иметь тёмный фон независимо от темы страницы.

Ключевое слово only

Добавление only предотвращает переопределение схемы каскадом для данного элемента:

.forced-light {
  color-scheme: only light;
}

Это прежде всего полезно, когда есть элемент внутри тёмного контекста, который должен оставаться светлым.

Замена медиазапросов prefers-color-scheme

Традиционный подход к тёмному режиму с медиазапросами требует дублирования или переопределения каждой цветовой переменной:

/* Традиционный подход — многословный */
:root {
  --bg: #FFFFFF;
  --text: #1A1A2E;
  --accent: #2563EB;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0F0F17;
    --text: #E8E8F0;
    --accent: #60A5FA;
  }
}

С light-dark() это сворачивается в:

/* Подход light-dark() — одно объявление на переменную */
:root {
  color-scheme: light dark;

  --bg:     light-dark(#FFFFFF, #0F0F17);
  --text:   light-dark(#1A1A2E, #E8E8F0);
  --accent: light-dark(#2563EB, #60A5FA);
}

Сами переменные становятся самодокументирующими. Читая --accent: light-dark(#2563EB, #60A5FA), вы сразу видите оба значения и понимаете их взаимосвязь. Подход с медиазапросом разбрасывает значения светлого и тёмного режима по двум отдельным блокам, что затрудняет аудит и обновление палитры.

Когда медиазапросы ещё нужны

Медиазапрос prefers-color-scheme остаётся необходимым для нецветовых адаптаций, меняющихся в зависимости от темы:

@media (prefers-color-scheme: dark) {
  /* Нецветовые корректировки, которые light-dark() не может выразить */
  img.logo {
    filter: invert(1) brightness(1.2);
  }

  .hero-image {
    opacity: 0.85;
  }
}

Для всего, что является исключительно изменением цвета, light-dark() чище. Для структурных или нецветовых адаптаций (фильтры изображений, непрозрачность, свойства отображения) медиазапрос остаётся правильным инструментом.

Совместное использование с CSS-кастомными свойствами

light-dark() работает внутри значений кастомных свойств — именно здесь раскрывается его полная мощь. Вы определяете все цвета с поддержкой тем в :root, и каждый компонент на странице ссылается на эти переменные. При смене цветовой схемы всё обновляется одновременно.

Пример полной системы тем

:root {
  color-scheme: light dark;

  /* Фоны */
  --color-bg-base:      light-dark(#FFFFFF,   #0F0F17);
  --color-bg-elevated:  light-dark(#F8F9FA,   #1A1A2E);
  --color-bg-overlay:   light-dark(#F1F3F5,   #252540);

  /* Текст */
  --color-text-primary:   light-dark(#1A1A2E, #E8E8F0);
  --color-text-secondary: light-dark(#4A4A6A, #A8A8C0);
  --color-text-muted:     light-dark(#6C757D, #6A6A88);
  --color-text-inverse:   light-dark(#FFFFFF, #1A1A2E);

  /* Рамки */
  --color-border:         light-dark(#DEE2E6, #2E2E4A);
  --color-border-strong:  light-dark(#ADB5BD, #4A4A6A);

  /* Интерактивные / бренд */
  --color-accent:         light-dark(#2563EB, #60A5FA);
  --color-accent-hover:   light-dark(#1D4ED8, #93C5FD);
  --color-accent-subtle:  light-dark(#DBEAFE, #1E3A5F);

  /* Обратная связь */
  --color-success:  light-dark(#16A34A, #4ADE80);
  --color-warning:  light-dark(#D97706, #FCD34D);
  --color-danger:   light-dark(#DC2626, #F87171);

  --color-success-bg: light-dark(#F0FDF4, #052E16);
  --color-warning-bg: light-dark(#FFFBEB, #2D1A00);
  --color-danger-bg:  light-dark(#FEF2F2, #2D0A0A);
}

Компоненты ссылаются на эти переменные, не зная ничего о темизации:

.card {
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  color: var(--color-text-primary);
}

.btn-primary {
  background: var(--color-accent);
  color: var(--color-text-inverse);
}

.btn-primary:hover {
  background: var(--color-accent-hover);
}

.alert-success {
  background: var(--color-success-bg);
  color: var(--color-success);
  border-left: 3px solid var(--color-success);
}

Обратите внимание: светлый акцент #2563EB в тёмном режиме меняется на #60A5FA. Это намеренно: синий 600-го оттенка проходит WCAG AA по контрасту с белым, но не с тёмным фоном. Синий 400-го оттенка достаточно светлый для достижения необходимого контраста на тёмных поверхностях. Используйте Проверщик контраста для верификации комбинаций и Генератор оттенков для подбора правильного оттенка в каждом режиме.

Вложение light-dark() в другие функции

light-dark() возвращает цветовое значение, поэтому его можно использовать везде, где допустим цвет, — в том числе внутри других функций:

:root {
  color-scheme: light dark;
  --brand: #3B82F6;

  /* light-dark() внутри color-mix() */
  --brand-surface: color-mix(
    in oklch,
    var(--brand) 15%,
    light-dark(white, #09090b)
  );
}

Это создаёт цвет поверхности — 15% тинт фирменного цвета, смешанный с белым в светлом режиме и почти-чёрным в тёмном, — автоматически адаптированный к теме.

Добавление пользовательского переключения (JavaScript)

Реакция на системные предпочтения — правильный вариант по умолчанию, но пользователи должны иметь возможность переключаться самостоятельно. Для этого нужен JavaScript для сохранения выбора и переопределения браузерного значения по умолчанию.

Управление color-scheme через JavaScript

Ключевой момент: color-scheme — CSS-свойство, которое можно задать через JavaScript:

// Программно установить цветовую схему
document.documentElement.style.colorScheme = 'dark';
document.documentElement.style.colorScheme = 'light';

// Убрать переопределение (возврат к системным предпочтениям)
document.documentElement.style.colorScheme = '';

Когда color-scheme задаётся через inline-стиль на корневом элементе, он переопределяет объявление в таблице стилей. Все значения light-dark() разрешаются в подходящий вариант заново.

Полная реализация переключения

const STORAGE_KEY = 'color-scheme-preference';

function initColorScheme() {
  const stored = localStorage.getItem(STORAGE_KEY);
  if (stored === 'light' || stored === 'dark') {
    document.documentElement.style.colorScheme = stored;
  }
  // Если предпочтения не сохранены, CSS color-scheme: light dark; обрабатывает это через предпочтение ОС
}

function toggleColorScheme() {
  const current = getComputedStyle(document.documentElement)
    .colorScheme
    .trim();

  // Определить следующее значение
  const next = current.includes('dark') ? 'light' : 'dark';

  document.documentElement.style.colorScheme = next;
  localStorage.setItem(STORAGE_KEY, next);

  // Обновить состояние кнопки переключения
  updateToggleButton(next);
}

function updateToggleButton(scheme) {
  const btn = document.getElementById('theme-toggle');
  if (!btn) return;
  btn.setAttribute('aria-label',
    scheme === 'dark' ? 'Переключить на светлый режим' : 'Переключить на тёмный режим'
  );
  btn.dataset.scheme = scheme;
}

// Запустить до первой отрисовки, чтобы избежать мигания
initColorScheme();

// Привязать к кнопке переключения после готовности DOM
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('theme-toggle')
    ?.addEventListener('click', toggleColorScheme);
});

Вызов initColorScheme() до полного разбора DOM критически важен. Если выполнить позже, пользователи с сохранёнными предпочтениями увидят кратковременную тему по умолчанию до переключения — классическое «мигание неправильной темы». Размещайте этот скрипт inline в <head> или осторожно используйте атрибут defer (скрипты с defer выполняются после разбора DOM, что может быть слишком поздно).

Паттерн предотвращения мигания

Наиболее надёжный подход запускает минимальный inline-скрипт в <head>:

<head>
  <meta name="color-scheme" content="light dark">
  <script>
    const stored = localStorage.getItem('color-scheme-preference');
    if (stored) {
      document.documentElement.style.colorScheme = stored;
    }
  </script>
  <link rel="stylesheet" href="styles.css">
</head>

Этот скрипт выполняется синхронно до применения CSS, поэтому браузер вычисляет правильное значение color-scheme с первой отрисовки. Тег <meta name="color-scheme"> сообщает браузеру ожидаемые схемы ещё до разбора CSS — это предотвращает кратковременное белое мигание на страницах с тёмным режимом в некоторых браузерах.

Руководство по миграции с JS-базовых тем

Многие существующие реализации тёмного режима используют атрибут data-theme, переключаемый JavaScript, с CSS-переопределениями в области [data-theme="dark"]. Миграция на light-dark() выполняется постепенно — необязательно менять всё сразу.

Шаг 1: Добавьте color-scheme к :root

:root {
  color-scheme: light dark;
  /* Существующие кастомные свойства остаются без изменений */
}

Шаг 2: Мигрируйте переменные по одной

Начните с одной переменной как доказательства концепции. Замените паттерн с разделёнными объявлениями на унифицированный light-dark():

/* До */
:root {
  --bg: #FFFFFF;
}
[data-theme="dark"] {
  --bg: #0F0F17;
}

/* После */
:root {
  color-scheme: light dark;
  --bg: light-dark(#FFFFFF, #0F0F17);
}

Шаг 3: Обновите переключение

Измените JavaScript-переключение с установки data-theme на установку style.colorScheme:

/* До */
document.documentElement.setAttribute('data-theme', scheme);

/* После */
document.documentElement.style.colorScheme = scheme;

Шаг 4: Удалите селекторы data-theme

После миграции всех переменных удалите CSS-блоки [data-theme="dark"].

Резервный вариант для браузеров

Для примерно 15% браузеров без поддержки light-dark() предоставьте явные запасные варианты:

:root {
  /* Запасной вариант: явные значения светлого режима */
  --color-bg: #FFFFFF;
  --color-text: #1A1A2E;

  /* Прогрессивное улучшение с light-dark() */
  --color-bg:   light-dark(#FFFFFF, #0F0F17);
  --color-text: light-dark(#1A1A2E, #E8E8F0);
}

/* Запасной тёмный режим для браузеров без light-dark() */
@supports not (color: light-dark(white, black)) {
  @media (prefers-color-scheme: dark) {
    :root {
      --color-bg: #0F0F17;
      --color-text: #E8E8F0;
    }
  }
}

Блок @supports not (color: light-dark(white, black)) применяется только к браузерам, не понимающим light-dark(). Современные браузеры полностью игнорируют его, поскольку отрицательное условие ложно.

Ключевые выводы

  • light-dark(<light-value>, <dark-value>) возвращает первый аргумент в светлой цветовой схеме и второй в тёмной. Это нативный CSS-способ выразить «этот цвет, адаптированный к текущей теме».
  • Требует установки CSS-свойства color-scheme на элементе (или предке). Всегда задавайте color-scheme: light dark на :root для автоматической адаптации через prefers-color-scheme.
  • Главное преимущество перед традиционным подходом с медиазапросами — размещение обоих значений темы в одном объявлении, делающее взаимосвязь светлого и тёмного вариантов явной и легко проверяемой.
  • Пользовательское переключение требует установки document.documentElement.style.colorScheme через JavaScript. Сохраняйте выбор в localStorage и применяйте его в inline-скрипте <head> до загрузки CSS, чтобы предотвратить мигание.
  • Миграция с систем на базе атрибута data-theme выполняется постепенно: перемещайте переменные по одной из паттерна переопределения [data-theme="dark"] в паттерн light-dark().
  • Поддержка браузерами составляет ~85% по состоянию на 2026 год. Используйте запасной вариант @supports not с блоком @media (prefers-color-scheme: dark) для старых браузеров.
  • Используйте Проверщик контраста для верификации соответствия WCAG обоих цветовых значений в каждой паре light-dark() и Генератор оттенков для подбора правильного оттенка в каждом режиме.

Похожие цвета

Похожие бренды

Похожие инструменты