Featured image of post Ключевые моменты CSS-проектирования для поддержки темного режима в блоге

Ключевые моменты CSS-проектирования для поддержки темного режима в блоге

Подробно рассматриваются продвинутое CSS-проектирование для реализации темного режима в блогах и веб-приложениях, меры по предотвращению FOUC, а также использование цветового пространства (OKLCH) с учетом доступности (WCAG 2.1 AAA).

В современной веб-разработке поддержка темного режима (Dark Mode) превратилась из просто «приятного дополнения» (Nice to have) в «обязательное требование» (Must have) для улучшения пользовательского опыта (UX). В частности, для медиа, предполагающих длительное чтение текста, таких как блоги и сайты с документацией, важность поддержки темного режима крайне высока, поскольку он снижает нагрузку на глаза пользователей и уменьшает расход заряда батареи устройств.

В этой статье мы глубоко погрузимся в технические проблемы, которых не избежать при внедрении темного режима в блог, и ключевые моменты CSS-проектирования с высокой степенью поддерживаемости с точки зрения фронтенд-инженера. Мы охватим все аспекты реализации темного режима: использование CSS Custom Properties (CSS-переменных), продвинутое управление JavaScript и интеграцию с SSR для предотвращения FOUC (Flash of Unstyled Content), цветовой дизайн (RGB, HSL и новейший OKLCH) для обеспечения доступности (WCAG 2.1 AAA), а также практические примеры кода с использованием Tailwind CSS.


1. Основы проектирования тем с использованием CSS Custom Properties (CSS-переменных)

В настоящее время наиболее стандартным и мощным методом реализации темного режима является подход с использованием CSS Custom Properties (CSS-переменных). В отличие от переменных в CSS-препроцессорах, таких как Sass ($color), которые разрешаются статически во время компиляции, CSS-переменные разрешаются и переопределяются динамически во время выполнения в браузере. Это позволяет мгновенно изменить цветовую гамму всей страницы, просто переключая классы с помощью JavaScript.

1.1 Базовое определение цветовой темы

Во-первых, мы используем псевдокласс :root для определения цветовой палитры светлого режима (по умолчанию). Затем классический паттерн проектирования заключается в переопределении этих переменных при добавлении атрибута, такого как [data-theme='dark'] (или класса .dark).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* Определение переменных светлого режима (по умолчанию) */
:root {
  --color-bg-primary: #ffffff;
  --color-bg-secondary: #f3f4f6;
  --color-text-primary: #111827;
  --color-text-secondary: #4b5563;
  --color-accent: #3b82f6;
  --color-border: #e5e7eb;
}

/* Переопределение переменных для темного режима */
[data-theme='dark'] {
  --color-bg-primary: #111827;
  --color-bg-secondary: #1f2937;
  --color-text-primary: #f9fafb;
  --color-text-secondary: #9ca3af;
  --color-accent: #60a5fa;
  --color-border: #374151;
}

/* Фактическое применение */
body {
  background-color: var(--color-bg-primary);
  color: var(--color-text-primary);
  transition: background-color 0.3s ease, color 0.3s ease;
}

a {
  color: var(--color-accent);
}

Таким образом, полностью разделяя определение макета/типографики и цвета (темы), поддерживаемость CSS значительно повышается.

1.2 Использование @media (prefers-color-scheme: dark)

Если темный режим установлен на уровне ОС, с точки зрения UX желательно автоматически применять темную тему с момента первого посещения веб-сайта. Это достигается с помощью медиа-запроса @media (prefers-color-scheme: dark).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* Фолбэк, если в настройках ОС выбран темный режим */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --color-bg-primary: #111827;
    --color-bg-secondary: #1f2937;
    --color-text-primary: #f9fafb;
    --color-text-secondary: #9ca3af;
    --color-accent: #60a5fa;
    --color-border: #374151;
  }
}

При таком написании переменные переопределяются с учетом настроек темного режима ОС, если только пользователь явно не выбрал светлый режим (data-theme='light').


2. Понимание цветового пространства и доступность (WCAG 2.1 AAA)

При цветовом проектировании для темного режима недостаточно просто «сделать фон черным, а текст белым». Если контраст слишком сильный, это вызовет ореол и усложнит чтение, а если контраст слишком низкий, пострадает видимость. В Web Content Accessibility Guidelines (WCAG) строго определен коэффициент контрастности для обеспечения видимости.

2.1 Формула расчета коэффициента контрастности WCAG

Коэффициент контрастности (Contrast Ratio) $CR$ в WCAG определяется следующим образом с использованием относительной яркости (Relative Luminance) цвета фона и цвета переднего плана.

$$CR = \frac{L_{lighter} + 0.05}{L_{darker} + 0.05}$$

Здесь $L_{lighter}$ — относительная яркость более светлого цвета, а $L_{darker}$ — относительная яркость более темного цвета (диапазон значений от 0.0 до 1.0). Для достижения уровня AAA по стандарту WCAG 2.1 требуется коэффициент контрастности не менее 7:1 для обычного текста и ** не менее 4.5:1** для крупного текста.

Относительная яркость $L$ рассчитывается по следующей сложной формуле из значений RGB в цветовом пространстве sRGB.

$$L = 0.2126 \times R + 0.7152 \times G + 0.0722 \times B$$

Каждый компонент ($R, G, B$) использует нормализованное значение исходного 8-битного значения ($R_{sRGB}$), деленное на 255, и выполняет следующее преобразование для снятия гамма-коррекции.

$$ R, G, B = \begin{cases} \frac{C_{sRGB}}{12.92} & \text{if } C_{sRGB} \le 0.03928 \\ \left( \frac{C_{sRGB} + 0.055}{1.055} \right)^{2.4} & \text{otherwise} \end{cases} $$

Выполнять этот расчет вручную сложно, но, используя инструменты цветового проектирования, можно механически подобрать цвета, которые удовлетворяют коэффициенту контрастности 7:1 ($CR \ge 7.0$).

2.2 HSL vs RGB vs OKLCH

При создании цветовой палитры раньше доминировали RGB и HSL. Однако они имеют серьезный недостаток с точки зрения «перцептивной однородности» (perceptual uniformity).

  • RGB: Это механические три основных цвета света, и людям сложно интуитивно вносить коррективы вроде «сделать светлее» или «сделать темнее».
  • HSL: Использует оттенок (Hue), насыщенность (Saturation) и светлоту (Lightness), но «светлота (L)» в HSL не совпадает с воспринимаемой яркостью человеческим глазом. Например, чистый желтый и чистый синий со светлотой 50% в HSL численно имеют одинаковую яркость, но человеческому глазу желтый кажется значительно ярче.
  • OKLCH: Это новейшее цветовое пространство, введенное в CSS Color Module Level 4. Оно состоит из светлоты (Lightness — перцептивной яркости), насыщенности (Chroma) и оттенка (Hue), и ** полностью соответствует зрительным характеристикам человека (перцептивно однородно)**.

Используя OKLCH, вы можете сохранять ту же перцептивную яркость (Lightness) даже при изменении оттенка (Hue), что делает создание цветовой палитры для темного режима чрезвычайно предсказуемым и безопасным.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Пример определения CSS-переменных с использованием OKLCH */
:root {
  /* Светлый режим: высокая базовая яркость, умеренная насыщенность */
  --bg-base: oklch(0.98 0.01 250);
  --text-base: oklch(0.25 0.02 250);
  --primary-brand: oklch(0.65 0.15 250);
}

[data-theme="dark"] {
  /* В темном режиме просто инвертируем яркость, легче поддерживать перцептивный контраст */
  --bg-base: oklch(0.20 0.02 250);
  --text-base: oklch(0.95 0.01 250);
  --primary-brand: oklch(0.75 0.15 250); /* Слегка осветляем для темного режима для обеспечения видимости */
}

Приняв OKLCH таким образом, можно просто построить логику, гарантирующую согласованный коэффициент контрастности (уровень WCAG AAA) в нескольких темах.


3. Предотвращение FOUC (Flash of Unstyled Content) и SSR гидратация

Наиболее раздражающей проблемой для разработчиков при реализации темного режима является мерцание экрана, известное как FOUC (Flash of Unstyled Content).

3.1 Ловушка переключения тем с помощью клиентского JS

В SPA, таких как React или Vue (или статических сайтах с помощью SSG), распространена практика сохранения пользовательских настроек в localStorage и их чтения с помощью JavaScript для переключения темы. Однако если этот процесс выполняется в useEffect в React или аналогичном хуке, возникают следующие проблемы:

  1. Браузер отрисовывает HTML/CSS светлого режима.
  2. Загружается и выполняется JS-бандл.
  3. Из localStorage считывается настройка dark.
  4. В HTML добавляется класс dark, и экран внезапно темнеет (мерцание).

Лучшая практика для полного предотвращения FOUC и предотвращения ошибок гидратации — это сохранение пользовательских настроек темы в document.cookie и возврат HTML с соответствующими классами, добавленными на этапе серверного рендеринга (SSR).

Следующая диаграмма последовательности иллюстрирует идеальный процесс инициализации темы с использованием Cookie.

  sequenceDiagram
    participant User as "Пользователь"
    participant Browser as "Браузер ОС"
    participant Server as "Сервер SSR"
    participant DOM as "Клиентский DOM"

    User->>Browser: "Запрос страницы"
    Browser->>Server: "GET-запрос с Cookie (theme=dark)"
    Server-->>Browser: "Возврат HTML с <html data-theme='dark'>"
    Browser->>DOM: "Рендеринг начального HTML (Без FOUC)"
    DOM->>DOM: "Выполнение скрипта гидратации"
    alt "Cookie отсутствует (Первый визит)"
        DOM->>Browser: "Проверка OS prefers-color-scheme"
        Browser-->>DOM: "Результат matchMedia (например, dark)"
        DOM->>DOM: "Установка data-theme='dark' через блокирующий инлайн-скрипт"
    end

Для блогов, которые используют только SSG (генерацию статических сайтов) и где SSR невозможен (например, Hugo, Gatsby, статический экспорт Astro), необходимо разместить блокирующий инлайн-скрипт JavaScript внутри тега <head> для добавления класса непосредственно перед отрисовкой DOM.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- Размещается в конце тега <head> -->
<script>
  (function() {
    try {
      var localTheme = localStorage.getItem('theme');
      var osTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
      var theme = localTheme || osTheme;
      document.documentElement.setAttribute('data-theme', theme);
    } catch (e) {}
  })();
</script>

Поскольку этот небольшой скрипт блокирует рендеринг браузера и выполняется немедленно, атрибут data-theme уже установлен к моменту отрисовки экрана, что полностью предотвращает мерцание экрана (FOUC).


4. Подходы к реализации с использованием Tailwind CSS и чистого SCSS/CSS

При внедрении темного режима в реальный проект необходимо понимать подходы, специфичные для каждого инструмента.

4.1 Темный режим в Tailwind CSS

Tailwind CSS предоставляет вариант dark: по умолчанию, что делает реализацию темного режима чрезвычайно простой. Вы настраиваете свойство darkMode в конфигурационном файле (tailwind.config.js).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// tailwind.config.js
module.exports = {
  // 'media' (зависит от настроек ОС) или 'class' (переключается вручную)
  darkMode: 'class', 
  theme: {
    extend: {
      colors: {
        /* Расширение цветовой палитры Tailwind с использованием CSS-переменных */
        primary: 'rgb(var(--color-primary) / <alpha-value>)',
        background: 'rgb(var(--color-background) / <alpha-value>)',
      }
    }
  }
}

На стороне HTML вы просто добавляете классы следующим образом:

1
2
3
4
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
  <h1 class="text-2xl font-bold">Hello World</h1>
  <p class="mt-2">Tailwind makes dark mode incredibly easy.</p>
</div>

Однако, написание dark:bg-xxx для каждого элемента может привести к раздуванию компонентов. В крупных блогах или приложениях рекомендуется гибридный подход (семантический цветовой дизайн), где CSS-переменные служат основой, а Tailwind ссылается на эти CSS-переменные.

Ниже представлена диаграмма классов, показывающая наследование CSS-переменных и уровни применения.

  classDiagram
    class GlobalCSSVariables {
        "--color-brand-500"
        "--color-gray-900"
    }
    class SemanticVariables {
        "--bg-primary"
        "--text-base"
        "--accent"
    }
    class TailwindConfig {
        "theme.colors.background"
        "theme.colors.primary"
    }
    class UIComponents {
        "class='bg-background text-primary'"
    }

    GlobalCSSVariables <|-- SemanticVariables : #58;root & .dark
    SemanticVariables <|-- TailwindConfig : tailwind.config.js
    TailwindConfig <.. UIComponents : Применяет утилитарные классы

4.2 Реализация на чистом SCSS/CSS (Использование примесей/Mixin)

В проектах, которые не используют Tailwind, а пишут собственные SCSS, стили темного режима инкапсулируются с помощью @mixin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* Определение SCSS Mixin */
@mixin dark-mode {
  /* Поддерживает как атрибут [data-theme='dark'], так и настройки ОС */
  [data-theme='dark'] & {
    @content;
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) & {
      @content;
    }
  }
}

/* Пример использования */
.card {
  background-color: #ffffff;
  color: #333333;
  border: 1px solid #eeeeee;

  @include dark-mode {
    background-color: #1a202c;
    color: #e2e8f0;
    border-color: #2d3748;
  }
}

Хотя этот метод интуитивно понятен, скомпилированный файл CSS имеет тенденцию раздуваться (поскольку медиа-запросы дублируются для каждого селектора), поэтому современным трендом является переход к дизайну, ориентированному на CSS-переменные (Custom Properties).


5. Оптимизация изображений (Image) и SVG для темного режима

Даже если цветовой дизайн текста и фона завершен, если изображения и иконки (SVG), размещенные в качестве контента, останутся в светлом режиме, они будут выглядеть слишком яркими и неуместными в темном режиме. Оптимизация этих элементов также имеет важное значение.

5.1 CSS-фильтры для снижения яркости изображений

Растровые изображения, такие как фотографии, могут быть слишком яркими, если они отображаются как есть в темном режиме. Используя свойство CSS filter для незначительного снижения яркости (brightness) и контрастности (contrast) изображений, вы можете заставить их естественным образом сливаться с пользовательским интерфейсом темной темы.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[data-theme='dark'] img:not([src*=".svg"]) {
  /* Уменьшаем яркость, немного увеличиваем контраст */
  filter: brightness(0.8) contrast(1.1);
  transition: filter 0.3s ease;
}

[data-theme='dark'] img:hover {
  /* Возвращаем исходную яркость при наведении (если пользователь хочет рассмотреть детали) */
  filter: brightness(1) contrast(1);
}

5.2 Переключение изображений с помощью тега <picture>

Изображения логотипов и пояснительные схемы (например, JPEG с фиксированным белым фоном) не могут быть адаптированы только с помощью фильтров. В этом случае правильным подходом будет использование элемента HTML <picture> и медиа-запросов для предоставления отдельного файла изображения для темного режима.

1
2
3
4
5
6
<picture>
  <!-- Показывать это пользователям с настройками ОС в темном режиме -->
  <source srcset="/img/logo-dark.png" media="(prefers-color-scheme: dark)">
  <!-- По умолчанию (светлый режим) -->
  <img src="/img/logo-light.png" alt="Логотип сайта">
</picture>

※ Однако этот метод не связан с ручным переключением через localStorage и т.д. (он зависит только от настроек ОС). Поэтому, если вы реализуете ручное переключение, вам нужно либо динамически переписывать атрибут src изображений с помощью JS, либо переключать display: none с помощью CSS-классов.

5.3 Использование currentColor для SVG-иконок

Самый элегантный способ работы с инлайн-SVG, используемыми для иконок, — это связать цвет заливки с цветом текста родительского элемента. Задайте currentColor для атрибутов fill или stroke в SVG.

1
2
3
4
<!-- Значение свойства CSS color (например, var(--text-primary)) применяется автоматически -->
<svg viewBox="0 0 24 24" fill="currentColor">
  <path d="M12 2L2 22h20L12 2z" />
</svg>

Благодаря этому, когда включается темный режим и цвет текста родительского элемента становится светлым/белым, SVG-иконка также автоматически меняется на светлый/белый.


6. Заключение: К устойчивому проектированию темного режима

Чтобы реализовать качественный темный режим в блоге или веб-приложении, необходимо CSS-проектирование, охватывающее следующие ключевые моменты:

  1. Использование CSS Custom Properties: Избегайте жесткого кодирования (хардкода) цветов и абстрагируйте их в семантические имена переменных (например, --bg-primary).
  2. Принятие цветового пространства OKLCH: Логическое проектирование высокодоступного коэффициента контрастности (7:1 или выше), соответствующего WCAG 2.1 AAA, в перцептивно однородном цветовом пространстве.
  3. Строгие меры предотвращения FOUC: Полное устранение мерцания экрана при начальной загрузке путем связывания SSR и Cookie или блокирующего инлайн-скрипта в <head>.
  4. Оптимизация медиа и ассетов: Использование filter: brightness(), currentColor и тегов <picture>, чтобы нетекстовые элементы гармонировали с темной темой.

Можно сказать, что эти тщательные соображения, выходящие за рамки простого «инвертирования цветов», являются отличительной чертой современного блога, который обеспечивает превосходный опыт чтения, любим пользователями в течение долгого времени и меньше утомляет глаза. Разработчикам, которые собираются внедрять темный режим, обязательно стоит использовать паттерны проектирования из этой статьи в качестве справочного материала.

comments powered by Disqus