Theme Architecture
theme architecture theme architecture defines how visual variants — light, dark, high-contrast, brand a/b — propagate through css without duplicating
Introduction
Theme architecture defines how visual variants — light, dark, high-contrast, brand A/B — propagate through CSS without duplicating components. Staff patterns: class on html, data-theme attribute, prefers-color-scheme default with manual override, and per-tenant token injection for white-label.
Theme architecture is judged by whether one component file supports all brands without conditional React branches for every color.
Business problem
Business pressure: Dark mode launch broke 200 components because colors were hardcoded. Theme architecture assigns every color to a semantic token that remaps per theme — not per-component dark: variants scattered in 800 files.
- User preference: OS dark mode + in-app toggle must persist without flash.
- Brand: Enterprise tier gets custom theme without forked repo.
- SSR: Nuxt/Next must emit correct theme class before paint — no flash.
Why this feature exists
Platform history: Sass $theme maps preceded CSS variables. Custom properties enabled runtime theme swap — Carbon and Material rebuilt theming around CSS variables + data attributes.
- Problem solved: N duplicate stylesheets per theme.
- Modern role: Single CSS bundle, multiple theme maps on :root or [data-theme].
Browser rendering perspective
Rendering impact: Theme switch toggles class — custom properties recalculate — repaint not full relayout if only colors change. Avoid swapping entire CSS files — cache miss and FOUC.
Internal browser workflow
Flow: Load base tokens → apply theme map → components use semantic vars → user toggle updates attribute → localStorage sync → optional SSR cookie for first paint.
Feature deep dive
Theme map pattern:
:root, [data-theme="light"] {--color-surface: #ffffff;--color-text: #0f172a;}[data-theme="dark"] {--color-surface: #0f172a;--color-text: #f8fafc;}.card { background: var(--color-surface); color: var(--color-text); }
Syntax
Carbon: @carbon/themes with g10, g90, white themes. Material: theme mixin generates token sets.
Examples
SSR anti-FOUC script (inline in head):
<script>(function(){var t = localStorage.theme || 'light';document.documentElement.setAttribute('data-theme', t);})();</script>
Real-world use
Carbon theme packages switch entire token sets. Atlassian theming API on Atlaskit components. Polaris ThemeProvider. Material Web CSS tokens with light/dark schemes.
Real production example
Pattern: Theme provider sets data-theme; all DS components consume semantic tokens only; Percy snapshots per theme.
Enterprise use case
Enterprise: Reference Material theme layering — reference → system → component. Atlassian design tokens split global from product-specific themes.
- Carbon: g100, g90, g10, white — documented theme matrix.
- Polaris: ThemeProvider + tokens for Shopify admin consistency.
Accessibility considerations
A11y: High-contrast theme separate from dark — data-theme="hc" with audited pairs. Respect prefers-contrast: more.
Performance considerations
Performance: One bundle beats per-theme CSS files. Theme toggle should not reload page.
SEO considerations
SEO: FOUC from wrong theme hurts perceived quality; inline head script prevents flash on marketing SSR pages.
Scalability considerations
Scale: Tenant theme JSON merged at build or runtime — architecture must define which tokens are tenant-overridable.
Common production issues
Failures: Hardcoded #fff in dark mode. Chart libraries ignoring theme tokens.
Debugging guide
Debug: Toggle data-theme in DevTools; inspect --color-* cascade on :root.
Best practices
- Semantic tokens only in components.
- SSR inline script for theme before CSS.
- Snapshot tests per theme.
- Separate high-contrast theme map.
Anti-patterns
- Duplicate component SCSS for dark.
- Loading second CSS file on theme toggle.
Trade-offs
- CSS variables: runtime flexible; IE legacy dead.
- Sass maps: build-time only; simpler mental model historically.
Architecture review questions
- How many theme dimensions — color only or density too?
- Tenant theme override API documented?
Interview questions
Design dark mode architecture without duplicating components.(Advanced)
Semantic tokens on :root remapped per [data-theme="dark"]. Components use var(--color-surface) never raw hex. SSR inline script sets theme before paint. Percy per theme.
Follow-up: Carbon vs custom variable themes?
Hands-on exercise
Exercise: Light/dark toggle with data-theme, no component color changes — only token maps.
Staff engineer notes
- Theme architecture = token semantics + root attribute + SSR — not "add dark: Tailwind everywhere".
Common pitfalls
- Third-party widgets outside token system.
Try it yourself
Edit the CSS panel — the preview updates live. Use DevTools Performance and Accessibility panels to validate.
Try it yourself
Summary
Theme architecture propagates visual variants through semantic token remapping — following patterns from Material, Carbon, Atlassian, and Polaris rather than per-component color forks.
Key takeaways
- Theme maps remap semantic tokens on root attribute.
- SSR script prevents FOUC on theme.