frontend/design-systems
Dark Mode Across Eight Apps, From One Source of Truth
Adding dark mode to one app is an afternoon. Adding it to a suite of eight, plus a configurable per-tenant accent color, without reimplementing it eight times, is an architecture problem. The trick is to make the shared component library the only place that knows what a color means.
Eight apps, one library
The product is a suite — People, Global, Inventory, Docs, Tasks, Calendar, Partners, and more — all consuming a shared punica-components library. If theming lived in each app, dark mode would be eight independent projects that drift apart immediately. So the tokens had to live in the library, and every app had to inherit them.
The goal wasn’t just light/dark. It was a master color: a tenant picks an accent, and it propagates everywhere without anyone touching per-app CSS.
Tokens, not hex codes
The move is to stop letting components name colors and start letting them name roles. A button doesn’t want `#f59e0b`; it wants `--color-accent`. Once every surface references semantic tokens, switching themes is swapping a token set, and the master color is one variable feeding the rest.
:root {
--color-accent: var(--tenant-master-color, #f59e0b);
--color-bg: #ffffff;
--color-surface: #f4f4f5;
--color-text: #171717;
}
:root[data-theme="dark"] {
--color-bg: #000000;
--color-surface: #141414;
--color-text: #f5f5f5;
/* --color-accent inherits the tenant master color unchanged */
}The long tail lives in the dark
Centralizing tokens gets you 80% for free. The remaining 20% is every surface that hardcoded a color before the system existed: text inputs with the wrong background, table header sorters that vanished, upload buttons that disappeared against a dark panel, rich-text editors with unreadable contrast. None of these show up in the light theme, so they arrive as a flood the moment dark mode ships.
That’s why the rollout was phased and then followed by dedicated bulk-bug passes — clearing the dark-mode-only regressions app by app rather than pretending the token migration caught everything.
Why phasing beats a big bang
Shipping the theme to one app first surfaced the categories of regression cheaply, before they multiplied across eight codebases. By the time the later apps adopted the shared tokens, the common offenders were already fixed in the library. Phase, observe, harden, repeat — the same rhythm that works for anything touching a lot of surface area at once.