Developers love a quick win. When the ticket says "add dark mode," the path of least resistance looks obvious: write light.css, write dark.css, and toggle between them. It feels clean. It ships fast. For a tiny side project with three components, it might even hold up. But once your application grows past a handful of modules, that second file stops being an asset and becomes a liability you maintain in duplicate.
The Two-File Trap
The logic seems sound at first glance. Separation of concerns, right? Light things over here, dark things over there. You open two buffers in your editor. You copy the card styles from the light file into the dark file, swap #ffffff for #1a1a1a, and call it a day.
The problem is not the first week. The problem is month six, when a designer asks for a slightly different border radius on the primary button, or when the product team wants a new warning state on the checkout form. You update the light stylesheet. You eyeball the dark stylesheet. Maybe you remember to copy the change over. Maybe you do not. That gap is where quality dies. You are no longer maintaining one interface. You are maintaining two parallel interfaces that happen to share the same HTML skeleton.
Theme Drift Is Inevitable
This gap has a name that frontend teams are starting to recognize: theme drift. It happens when your two stylesheets evolve at different speeds. A padding adjustment here. A shadow tweak there. The dark file becomes the neglected sibling. Or worse, it becomes a source of fear. Developers start avoiding changes because touching one theme means hunting through another file to duplicate the work.
The cognitive overhead compounds fast. You wanted to write CSS once. Instead you wrote it twice, and now you are paying interest on that debt every time the design system shifts. Icons misalign in dark mode because someone updated a flex gap in the light file and forgot to mirror it. Focus rings disappear because a new accessibility rule only made it into one sheet. The UI does not just look wrong. It starts to feel broken.
Use Semantic Tokens
The fix is not a better diff tool or stricter code review. The fix is a different way of thinking about color. Stop organizing your styles by literal appearance and start organizing them by purpose. This is where semantic tokens come in.
Instead of assigning a card a white background, assign it a surface background. Instead of choosing between black and off-white for text, choose a text color. The component does not know or care whether the user prefers light or dark. It simply asks for the token that matches its job.
Think about a standard button. In a two-file world, .btn lives in the light stylesheet with a white background and a dark border. Its twin lives in the dark stylesheet with a near-black background and a lighter border. That is twice the code for one button. With tokens, .btn has one declaration: the background is var(--color-surface-secondary) and the border is var(--color-border-default). The values themselves live at the root. When the site is in light mode, --color-surface-secondary resolves to something like #f8f9fa. In dark mode, the same token resolves to #2d2d2d. The button component never changes. Only the data underneath it does.
This distinction between structure and data is subtle but powerful. Your card component defines layout, spacing, typography, and elevation once. Your theme layer defines the palette. That separation is exactly what CSS custom properties were built for.
How the Architecture Changes
This approach fundamentally restructures how you write styles.
The old way usually looks like this:
- A light card stylesheet defining padding, radius, background, text color, and shadow.
- A dark card stylesheet redefining most of the same properties just to flip colors.
- A logic layer deciding which stylesheet to load or which class to toggle on the body.
The new way looks like this:
- One card stylesheet defining layout and assigning semantic tokens.
- One theme file defining what those tokens mean in a light context.
- One theme file, or simply a block in the same file, defining what those tokens mean in a dark context.
- A single attribute swap that changes the value layer without touching the component layer.
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.
