Programiści uwielbiają szybkie sukcesy. Gdy w zadaniu widnieje „dodaj tryb ciemny”, droga najmniejszego oporu wydaje się oczywista: napisz light.css, napisz dark.css i przełączaj się między nimi. Wydaje się to czyste. Szybko gotowe. W małym projekcie pobocznym z trzema komponentami może to nawet zadziałać. Ale gdy aplikacja przekroczy kilka modułów, ten drugi plik przestaje być atutem, a staje się obciążeniem, które musisz utrzymywać w duplikacie.

Pułapka dwóch plików

Logika wydaje się słuszna na pierwszy rzut oka. Separacja odpowiedzialności, prawda? Jasne rzeczy tutaj, ciemne tam. Otwierasz dwa bufory w edytorze. Kopiujesz style karty z jasnego pliku do ciemnego, zamieniasz #ffffff na #1a1a1a i gotowe.

Problem nie pojawia się w pierwszym tygodniu. Problem pojawia się w szóstym miesiącu, gdy projektant prosi o nieco inny border-radius przy głównym przycisku lub gdy zespół produktowy chce nowego stanu ostrzegawczego w formularzu płatności. Aktualizujesz jasny arkusz stylów. Przeglądasz ciemny arkusz stylów „na oko”. Może pamiętasz o skopiowaniu zmiany. Może nie. To właśnie w tej luce umiera jakość. Nie utrzymujesz już jednego interfejsu. Utrzymujesz dwa równoległe interfejsy, które przypadkiem współdzielą ten sam szkielet HTML.

Rozbieżność motywów jest nieunikniona

Ta luka ma nazwę, którą zespoły frontendowe zaczynają rozpoznawać: theme drift (rozbieżność motywów). Dzieje się tak, gdy dwa arkusze stylów ewoluują w różnym tempie. Tu korekta paddingu, tam poprawka cienia. Ciemny plik staje się zaniedbanym rodzeństwem. Albo co gorsza, staje się źródłem strachu. Programiści zaczynają unikać zmian, ponieważ dotknięcie jednego motywu oznacza konieczność przeszukiwania drugiego pliku, aby powielić pracę.

Narzut poznawczy rośnie błyskawicznie. Chciałeś napisać CSS raz. Zamiast tego napisałeś go dwa razy, a teraz płacisz odsetki od tego długu za każdym razem, gdy system projektowy ulega zmianie. Ikony rozjeżdżają się w ciemnym trybie, bo ktoś zaktualizował flex gap w jasnym pliku i zapomniał go odzwierciedlić. Obramowania skupienia (focus rings) znikają, bo nowa zasada dostępności trafiła tylko do jednego arkusza. UI nie tylko wygląda źle. Zaczyna sprawiać wrażenie zepsutego.

Używaj tokenów semantycznych

Rozwiązaniem nie jest lepsze narzędzie do porównywania różnic (diff tool) ani surowszy code review. Rozwiązaniem jest inne podejście do kolorów. Przestań organizować style według dosłownego wyglądu, a zacznij organizować je według przeznaczenia. Tu wchodzą tokeny semantyczne.

Zamiast przypisywać karcie białe tło, przypisz jej tło typu surface. Zamiast wybierać między czarnym a złamaną bielą dla tekstu, wybierz kolor tekstu. Komponent nie wie ani nie obchodzi go, czy użytkownik woli tryb jasny, czy ciemny. Po prostu prosi o token, który odpowiada jego zadaniu.

Pomyśl o standardowym przycisku. W świecie dwóch plików .btn żyje w jasnym arkuszu stylów z białym tłem i ciemną obwódką. Jego bliźniak żyje w ciemnym arkuszu z niemal czarnym tłem i jaśniejszą obwódką. To dwa razy więcej kodu dla jednego przycisku. Dzięki tokenom .btn ma jedną deklarację: tło to var(--color-surface-secondary), a obwódka to var(--color-border-default). Same wartości znajdują się w korzeniu (root). Gdy strona jest w jasnym trybie, --color-surface-secondary przyjmuje wartość typu #f8f9fa. W ciemnym trybie ten sam token przyjmuje wartość #2d2d2d. Komponent przycisku nigdy się nie zmienia. Zmieniają się tylko dane pod nim.

To rozróżnienie między strukturą a danymi jest subtelne, ale potężne. Twój komponent karty definiuje układ, odstępy, typografię i elevation raz. Twoja warstwa motywu definiuje paletę. Ta separacja jest dokładnie tym, do czego stworzono właściwości CSS (custom properties).

Jak zmienia się architektura

To podejście fundamentalnie zmienia sposób pisania stylów.

Stary sposób zazwyczaj wygląda tak:

  • Jasny arkusz stylów karty definiujący padding, radius, tło, kolor tekstu i cień.
  • Ciemny arkusz stylów karty redefiniujący większość tych samych właściwości tylko po to, by odwrócić kolory.
  • Warstwa logiki decydująca, który arkusz stylów załadować lub którą klasę przełączyć na elemencie body.

Nowy sposób wygląda tak:

  • Jeden arkusz stylów karty definiujący układ i przypisujący tokeny semantyczne.
  • Jeden plik motywu definiujący, co te tokeny oznaczają w jasnym kontekście.
  • Jeden plik motywu (lub po prostu blok w tym samym pliku) definiujący, co te tokeny oznaczają w ciemnym kontekście.
  • Pojedyncza zamiana atrybutu, która zmienia warstwę wartości bez dotykania warstwy komponentu.

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.