case study
LinkedIn Dark Mode
A two-year token migration that turned dark mode from a setting into infrastructure.
Dark mode looks like a toggle and is actually a token rewrite. On a product the size of LinkedIn, “ship dark mode” means inventory every hardcoded hex in the codebase, decide what it meant semantically, and then thread a semantic token through enough surfaces that the toggle has somewhere to act.
Context
Most “dark mode ship-it” failures aren’t visual — they’re organisational. The team owning a particular surface has its own color values, its own brand opinions, its own deadlines. A central design-systems push needs a migration story that lets every surface land at its own pace without forking the palette.
Move
People think dark mode is easy. It’s a prefers-color-scheme media query and a second palette. At a company where every pillar team owns part of the application, it isn’t — it’s a years-long mindset shift that touches every PR.
The first problem was getting the org used to thinking in semantics. Everyone was fluent in “hey this is blue70” — but in a themable, scheme-aware world blue70 has no meaning. It’s a leaf value, not a contract. The token model I landed on was strictly semantic — --color-action-primary, --color-surface-elevated, --color-text-subtle — and the value of that token lived in one place per mode. The naming mistake I rejected early was tokens named by the property they ended up on (--color-button-bg); property-named tokens trap you in the next refactor, semantic ones survive it. Getting people to use the new model took hosting office hours, writing extensive documentation, and showing up in code reviews until the conversion of blue70 to --color-action-primary was a habit rather than a request.
The migration pattern was the next problem. LinkedIn has hundreds of surfaces and the team owning each one has its own deadlines. I couldn’t gate dark mode on every surface flipping at once; I also couldn’t ship a half-dark product. The compromise: every surface kept its hex values until the semantic layer was wired in, at which point a single token swap turned the whole surface dark-mode-correct in one PR. Teams could schedule that swap when it fit; the central system kept publishing the semantic layer ahead of them.
Theme tracking was the load-bearing decision underneath all of it. Parts of the application loaded inside iframes, which meant at any point we needed to know two things — the option the user had picked, and the current theme the application was actually running in:
- light → easy, the user picked light
- dark → easy, the user picked dark
- system → the user picked system, and the current theme depends on the OS
Because of the iframe case I deliberately moved away from a CSS-only prefers-color-scheme approach and built the theme layer on top of matchMedia — we tracked both the user’s choice and the live OS resolution, and passed an explicit theme prop into every embedded surface we didn’t fully control. CSS-only is elegant when you own the whole page; when half your surfaces are iframes, you need a value you can hand across the boundary.
The long tail was illustrations, brand colors, and third-party embeds. We didn’t try to make those adaptive — that’s a different design problem — we made them acceptable in either mode. A lightweight fallback layer handled the embedded surfaces we didn’t control: slightly desaturated treatments, mode-aware backgrounds where the artwork needed it, a sanctioned escape hatch for the colors that genuinely had to stay constant.
Outcome
The rollout was internal-first by design. Employees ran dark mode for weeks before any external user got it. That bought us the small-but-real bug list — the badge that read fine on white and disappeared on slate, the third-party embed with a hardcoded #fff background — and a clear picture of what complete meant. By the time we shipped externally, the long-tail bugs were in the changelog rather than in user reports.
The thing I’m most proud of isn’t visible in the toggle. It’s that semantic tokens kept paying off after dark mode shipped. The next brand refresh moved through the org in days, not months. A high-contrast variant slotted in on top of the same token layer. Dark mode stopped being a project and became infrastructure.
What I’d do differently
I’d reorder one step. We tried to migrate component-level surfaces before getting the layout shells onto semantic tokens, and that meant some components shipped looking right on a still-hardcoded background — a regression that read as “the system isn’t ready” even though it was. Layout shells first; components second.
I’d also push earlier on the gap between Figma color libraries and production semantics. Designers worked in named colors that didn’t map cleanly to the token model — close enough that we shipped it, far enough that every handoff needed a translation pass. That should have been a single source of truth between the platforms. Which is exactly the work I went on to build — see LinkedIn design tooling for the tokens-as-source-of-truth repo that came out of this era, and Superhuman design tooling for where it’s heading.