Carbon Themes vs. Custom Token Overrides: A Decision Guide for Light/Dark React Apps
Choosing between Carbon's four built-in themes and custom token overrides for a light/dark React app: a comparison of effort, risk, and accessibility, plus a working Theme component setup and validation steps.
28 Sept 2025, 00:04 UTC

The decision you're actually making
Your React app needs light and dark modes, and you're building on the IBM Carbon Design System. The real decision is not "which colors look nice" — it's whether you can ship with Carbon's four built-in themes (white, g10, g90, g100) or whether brand requirements force you into custom token overrides. That choice determines your maintenance burden for every future Carbon upgrade.
The short answer: use the built-in themes and semantic tokens unless a hard brand requirement makes that impossible. Custom overrides work, but you inherit responsibility for contrast ratios and migration churn that Carbon's default themes already solved.
Comparing the supported options
| Approach | Effort | Dark mode support | Upgrade risk | Accessibility |
|---|---|---|---|---|
| Built-in themes (white/g10/g90/g100) | Low — one attribute or Theme wrapper | Automatic via token remapping | Low | Contrast designed and tested by Carbon |
| Scoped themes via Theme component | Low — mixed themes per subtree | Automatic within each scope | Low | Inherited from built-in themes |
| Custom token overrides (Sass/CSS variables) | Medium–high | You must define both palettes | High — token names changed across v10→v11 | You must re-validate contrast yourself |
| Hard-coded hex values in components | Seems low, becomes high | Breaks — colors don't remap | High | Frequently fails in dark mode |
Why semantic tokens are the real engineering decision
Carbon exposes its themes as design tokens — named values like background, layer, and text-primary — delivered through Sass and CSS custom properties. When you switch from a light theme (white or g10) to a dark theme (g90 or g100), components that reference semantic tokens repaint themselves with no code changes, because the token values remap while the names stay constant.
The failure mode to avoid is reaching for raw color values. A card styled with background: #f4f4f4 looks fine in a light theme and becomes unreadable light-gray-on-dark-gray in g90. The same card styled with the layer token adapts in both directions. Every hard-coded hex you write is a dark-mode bug you haven't hit yet.
Carbon v11 leaned further into CSS custom properties, which has a practical benefit: theme switching happens at runtime by changing a scope attribute, with no stylesheet recompilation. If you're still on v10, expect a Sass-module-heavier workflow and different token names — check the migration notes for your installed major version before writing any token reference.
Implementing scoped themes in React
The supported path is the @carbon/react package. Its Theme component scopes a theme to a subtree, which also enables mixed-theme layouts — the classic case being a dark header over a light page body:
import { Theme, Header, HeaderName } from '@carbon/react';
function App() {
return (
<>
<Theme theme="g100">
<Header aria-label="My app">
<HeaderName href="#" prefix="IBM">Console</HeaderName>
</Header>
</Theme>
<Theme theme="white">
<main>{/* page content */}</main>
</Theme>
</>
);
}For a user-toggleable global dark mode, set the theme once at the root (via the Theme component around your app or the global theme attribute) and flip its value between white and g100 based on user preference. Because v11 themes are CSS custom properties, this flip is a DOM attribute change, not a rebuild.
For nested surfaces — a card inside a panel inside a page — use Carbon's Layer component instead of manually picking background colors per elevation. Layer applies contextual tokens that step through the layer model automatically, and it stays correct in both light and dark themes.
When custom overrides are justified — and what they cost
Override tokens only when a brand color is non-negotiable. The mechanism is supported: redefine token values through Sass or CSS variables inside a theme scope. But budget for three costs:
- Both palettes are yours now. You must define and maintain light and dark values for every token you touch.
- Contrast is yours now. Carbon's default themes were designed to meet accessibility contrast ratios; your overrides are not. A brand blue that passes on white may fail on g100.
- Migrations are yours now. Token names and the layering model evolved between v10 and v11. Overrides written against one major version may silently stop applying after an upgrade.
Also note the boundary of the system: third-party components that don't consume Carbon tokens will not respond to a theme switch. Wrap or restyle them separately, or isolate them visually.
Validating the result
- Check
package.jsonand confirm the token names you reference match your installed@carbon/reactmajor version's documentation. - Render a representative page with
Theme theme="g100"and again withwhite. Every Carbon-token-based component should change appearance with zero code edits; anything that doesn't is using a hard-coded value — find it with a search for hex literals in your styles. - Run an automated contrast check (axe DevTools or a similar linter) against both themes. This is mandatory if you overrode any token.
- Manually tab through interactive states (hover, focus, disabled) in dark mode — automated checks miss some state-specific token usage.
If all four checks pass on built-in themes, you're done. If you needed overrides, treat the contrast report as a release gate, not a suggestion.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.