Разработчики любят быстрые победы. Когда в тикете написано «добавить темную тему», путь наименьшего сопротивления кажется очевидным: написать light.css, написать dark.css и переключаться между ними. Это кажется чистым решением. Это позволяет быстро выпускать продукт. Для крошечного побочного проекта с тремя компонентами это может даже сработать. Но как только ваше приложение перерастает несколько модулей, второй файл перестает быть активом и становится обузой, которую приходится поддерживать в дублированном виде.

Ловушка двух файлов

На первый взгляд логика кажется здравой. Разделение ответственности, верно? Светлое — здесь, темное — там. Вы открываете два буфера в редакторе. Копируете стили карточки из светлого файла в темный, меняете #ffffff на #1a1a1a — и дело сделано.

Проблема проявляется не в первую неделю. Проблема — на шестой месяц, когда дизайнер просит немного изменить радиус скругления у основной кнопки или когда команда продукта хочет добавить новое состояние предупреждения в форму оформления заказа. Вы обновляете светлый файл стилей. Вы прикидываете на глаз темный файл стилей. Возможно, вы не забываете скопировать изменения. А возможно, и забываете. Именно в этом разрыве и умирает качество. Вы больше не поддерживаете один интерфейс. Вы поддерживаете два параллельных интерфейса, которые просто используют один и тот же HTML-скелет.

Дрейф темы неизбежен

У этого разрыва есть название, которое фронтенд-команды начинают узнавать: дрейф темы (theme drift). Это происходит, когда ваши два файла стилей развиваются с разной скоростью. Здесь подправили отступ. Там изменили тень. Темный файл становится «забытым братом». Или, что еще хуже, он становится источником страха. Разработчики начинают избегать изменений, потому что правка одной темы означает необходимость искать изменения в другом файле, чтобы продублировать работу.

Когнитивная нагрузка растет стремительно. Вы хотели написать CSS один раз. Вместо этого вы написали его дважды, и теперь каждый раз, когда дизайн-система меняется, вы платите проценты по этому долгу. Иконки смещаются в темном режиме, потому что кто-то обновил flex gap в светлом файле и забыл зеркально отразить это в темном. Индикаторы фокуса исчезают, потому что новое правило доступности попало только в один лист стилей. Интерфейс не просто выглядит неправильно. Кажется, что он сломан.

Используйте семантические токены

Решение — это не лучший инструмент для сравнения файлов (diff tool) и не более строгий код-ревью. Решение — это другой способ мышления о цвете. Перестаньте организовывать стили по их буквальному внешнему виду и начните организовывать их по назначению. Именно здесь на помощь приходят семантические токены.

Вместо того чтобы назначать карточке белый фон, назначьте ей фон поверхности (surface background). Вместо того чтобы выбирать между черным и почти белым цветом для текста, выберите «цвет текста». Компоненту все равно, какой режим предпочитает пользователь. Он просто запрашивает токен, соответствующий его задаче.

Подумайте об обычной кнопке. В мире двух файлов .btn живет в светлом файле стилей с белым фоном и темной рамкой. Его двойник живет в темном файле с почти черным фоном и более светлой рамкой. Это в два раза больше кода для одной кнопки. С токенами у .btn есть одно объявление: фон — это var(--color-surface-secondary), а рамка — var(--color-border-default). Сами значения живут в корне. Когда сайт находится в светлом режиме, --color-surface-secondary превращается, например, в #f8f9fa. В темном режиме тот же токен превращается в #2d2d2d. Компонент кнопки никогда не меняется. Меняются только данные под ним.

Это различие между структурой и данными тонкое, но мощное. Ваш компонент карточки один раз определяет макет, отступы, типографику и тени. Ваш слой темы определяет палитру. Именно для такого разделения и были созданы пользовательские свойства CSS (CSS custom properties).

Как меняется архитектура

Этот подход фундаментально перестраивает то, как вы пишете стили.

Старый способ обычно выглядит так:

  • Светлый файл стилей карточки, определяющий отступы, радиус, фон, цвет текста и тень.
  • Темный файл стилей карточки, переопределяющий большинство тех же свойств только ради смены цветов.
  • Слой логики, решающий, какой файл стилей загрузить или какой класс переключить на body.

Новый способ выглядит так:

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

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

Переключение через data-атрибут

Реализация может оставаться простой и читаемой. Примените data-атрибут к вашему HTML-тегу, например data-theme="dark", и позвольте определениям токенов работать в рамках этого атрибута.

Установите значения по умолчанию в :root для светлой темы, чтобы страница корректно отображалась еще до запуска JavaScript. Затем переопределите значения токенов внутри [data-theme="dark"]. Маленький скрипт отслеживает клик по переключателю, обновляет атрибут, и каждый компонент на странице мгновенно реагирует. Никакой «чехарды» с классами на отдельных элементах. Никакого импорта отдельного файла стилей прямо во время рендеринга. Браузер уже держит переменные в памяти; он просто перерисовывает страницу с новыми значениями.

Это делает ваш код чистым в самом практическом смысле. Вам не нужно использовать grep по двум директориям, чтобы найти каждый экземпляр .card. Вам не нужно беспокоиться о «войнах специфичности» между конфликтующими классами тем, наложенными на один и тот же узел. Ваш HTML остается читаемым. Ваш CSS остается централизованным и удобным для поиска.

Дело в значениях, а не в версиях

Темная тема — это про значения. Это не вторая версия вашего интерфейса. Углы вашей карточки не становятся более скругленными ночью. Ваша сетка не меняет свою форму. Вашей типографике не нужен новый ритм. Меняются только цвета, а иногда тени становятся чуть глубже. Относиться к темной теме как к полному рескину — это избыточное проектирование, которое превращает поддержку в кошмар.

Команды, которые делают это правильно, относятся к своей дизайн-системе как к базе данных. Компоненты запрашивают свойства по имени. Темы предоставляют записи. Переключение со светлой темы на темную — это изменение параметра запроса, а не переписывание схемы.

Именно такой подход спасает вас от «расползания» тем. Одна карточка. Одна кнопка. Один источник истины для отступов и размеров. Палитра живет в одном месте, логически сопоставлена и готова к любой среде, которую предпочитает пользователь.

Главный вывод

Если вы поддерживаете два CSS-файла для светлой и темной тем, вы не занимаетесь тематизацией. Вы занимаетесь клонированием. Переходите на семантические токены, ограничивайте их область действия через data-атрибут корневого уровня и позволяйте компонентам запрашивать роли вместо жестко прописанного внешнего вида. Первоначальный рефакторинг потребует усилий, но альтернатива — это бесконечная игра «прибей крота» в параллельных таблицах стилей. Жизнь слишком коротка, чтобы писать одну и ту же карточку дважды.