Розробники люблять швидкі перемоги. Коли в тікеті написано «додати темний режим», шлях найменшого опору здається очевидним: написати light.css, написати dark.css і перемикатися між ними. Це виглядає чисто. Це швидко релізиться. Для крихітного стороннього проєкту з трьома компонентами це навіть може спрацювати. Але щойно ваш застосунок переростає кілька модулів, цей другий файл перестає бути активом і стає обтяженням, яке доводиться підтримувати в дублях.
Пастка двох файлів
На перший погляд логіка здається слушною. Розподіл відповідальності, чи не так? Світле — тут, темне — там. Ви відкриваєте два буфери у своєму редакторі. Копіюєте стилі карток із світлого файлу в темний, замінюєте #ffffff на #1a1a1a і закінчуєте роботу.
Проблема не з'являється в перший же тиждень. Проблема з'являється на шостий місяць, коли дизайнер просить трохи змінити радіус заокруглення (border radius) для головної кнопки або коли продуктова команда хоче додати новий стан попередження у форму оформлення замовлення. Ви оновлюєте світлий таблицю стилів. Ви перевіряєте темний файл на око. Можливо, ви згадуєте скопіювати зміни. А можливо, ні. Саме в цій прогалині помирає якість. Ви більше не підтримуєте один інтерфейс. Ви підтримуєте два паралельні інтерфейси, які випадково мають однаковий HTML-скелет.
Дрейф теми неминучий
Цей розрив має назву, яку вже починають усвідомлювати фронтенд-команди: дрейф теми (theme drift). Це стається, коли ваші два таблиці стилів еволюціонують з різною швидкістю. Тут коригування відступів (padding). Там підправка тіні. Темний файл стає «занедбаним братом». Або ще гірше — він стає джерелом страху. Розробники починають уникати змін, тому що торкнутися однієї теми означає шукати зміни в іншому файлі, щоб продублювати роботу.
Когнітивне навантаження зростає лавиноподібно. Ви хотіли написати CSS один раз. Замість цього ви написали його двічі, і тепер ви сплачуєте відсотки за цим боргом щоразу, коли змінюється дизайн-система. Іконки зміщуються в темному режимі, тому що хтось оновив flex gap у світлому файлі й забув віддзеркалити це. Контури фокусу зникають, бо нове правило доступності потрапило лише в одну таблицю стилів. UI не просто виглядає неправильно. Здається, що він зламаний.
Використовуйте семантичні токени
Рішенням є не кращий інструмент для diff-ів або суворіше рев'ю коду. Рішенням є інший підхід до мислення про колір. Припиніть організовувати стилі за їхнім буквальним виглядом і почніть організовувати їх за призначенням. Саме тут на допомогу приходять семантичні токени.
Замість того, щоб призначати картці білий фон, призначте їй фон поверхні (surface background). Замість того, щоб обирати між чорним та майже білим для тексту, оберіть колір тексту. Компонент не знає і йому байдуже, чи віддає перевагу користувач світлому чи темному режиму. Він просто запитує токен, який відповідає його завданню.
Подумайте про стандартну кнопку. У світі двох файлів .btn живе у світлій таблиці стилів із білим фоном і темною рамкою. Його двійник живе в темній таблиці стилів із майже чорним фоном і світлішою рамкою. Це вдвічі більше коду для однієї кнопки. З токенами .btn має одне оголошення: фон — це var(--color-surface-secondary), а рамка — var(--color-border-default). Самі значення живуть у корені. Коли сайт у світлому режимі, --color-surface-secondary приймає значення на кшталт #f8f9fa. У темному режимі той самий токен приймає значення #2d2d2d. Компонент кнопки ніколи не змінюється. Змінюються лише дані під ним.
Ця різниця між структурою та даними тонка, але потужна. Ваш компонент картки визначає макет, відступи, типографіку та рівень висоти (elevation) лише один раз. Ваш шар теми визначає палітру. Саме для такого розділення і були створені CSS custom properties.
Як змінюється архітектура
Цей підхід фундаментально змінює структуру написання стилів.
Старий спосіб зазвичай виглядає так:
- Світлий файл стилів картки, що визначає padding, radius, background, колір тексту та тінь.
- Темний файл стилів картки, що перевизначає більшість тих самих властивостей лише для зміни кольорів.
- Рівень логіки, який вирішує, який файл стилів завантажити або який клас перемикати на тегу
body.
Новий спосіб виглядає так:
- Один файл стилів картки, що визначає макет і призначає семантичні токени.
- Один файл теми, що визначає значення цих токенів для світлого контексту.
- Один файл теми (або просто блок у тому ж файлі), що визначає значення цих токенів для темного контексту.
- Одне перемикання атрибута, яке змінює рівень даних, не чіпаючи рівень компонентів.
You keep the setup stable. You only change the data. When the designer wants to introduce a third theme, maybe a high-contrast mode or a midnight blue variant, you do not rewrite the card. You add one more assignment to the token map. The component stays dumb and happy. It still wants a surface color. The theme tells it which surface color to use.
The Data Attribute Switch
Implementation can stay simple and readable. Apply a data attribute to your HTML tag, something like data-theme="dark", and let your token definitions scope under it.
Set your defaults on :root for the light experience so the page renders correctly before JavaScript runs. Then override the token values under [data-theme="dark"]. A tiny script watches for a toggle click, updates the attribute, and every component on the page responds instantly. No class thrashing on individual elements. No importing an entirely separate stylesheet mid-render. The browser already has the variables in memory; it just repaints with new values.
This keeps your code clean in a very practical sense. You do not have to grep across two directories to find every instance of .card. You do not have to worry about specificity wars between competing theme classes stacked on the same node. Your HTML stays readable. Your CSS stays centralized and searchable.
It Is About Values, Not Versions
Dark mode is about values. It is not a second version of your UI. The corners of your card do not get rounder at night. Your grid does not collapse into a different shape. Your type scale does not need a new rhythm. Only the colors shift, and sometimes the shadows breathe a little deeper. Treating darkness as a full reskin is over-engineering that creates maintenance nightmares.
The teams that get this right treat their design system like a database. Components query for properties by name. Themes provide the records. Switching from light to dark is a query parameter change, not a schema rewrite.
That mindset is what saves you from theme drift. One card. One button. One source of truth for spacing and sizing. The palette lives in one place, logically mapped, ready for whatever environment the user prefers.
The Real Takeaway
If you are maintaining two CSS files for light and dark, you are not theming. You are cloning. Move to semantic tokens, scope them with a root-level data attribute, and let your components ask for roles instead of hardcoding appearances. The initial refactor takes effort, but the alternative is an endless game of whack-a-mole across parallel stylesheets. Life is too short to write the same card twice.
