GitHub has been running on 100% CSS Modules since June 2026. Getting there took roughly three years, more than 6,400 migrated sx props, a wrapper library built purely so the migration could be paused, and a theming problem that turned out to be the real blocker. Josh Black and Marie Lucca, both engineers on GitHub’s Primer design system, published the full account on September 25, 2026, including the numbers and the parts that went badly.

The engineering result is well documented: Primer saw 55% less server-side render time and 25% faster component initialisation by December 2024, and dotcom controllers saw per-controller SSR gains in the low tens of percent across 2025 and 2026. The process result is more useful to most readers: they migrated one component at a time behind a feature flag, verified with visual regression snapshots, and used a compatibility shim to avoid a big-bang cutover. Then they discovered the CSS was the easy part.

Key takeaways:

  • Two different numbers are often quoted as conflicting: 55% and 3–22%. They measure different things, roughly two years apart. Neither is wrong.
  • The winning argument for CSS Modules was not aesthetics. It was eliminating the client and server runtime entirely.
  • The wrapper package @primer/styled-react existed so that consumers of the old API could keep working while the underlying components were replaced underneath them.
  • Theming, not CSS, was the hard part. GitHub supports seven themes, each with a high-contrast variant, all wired through styled-components.
  • The final 895 props took three weeks and two engineers with coding agents. The other thousands took six months and eight.

What was actually breaking

The trigger was not a single incident. In 2023 the number of components on certain pages began to explode, and three things degraded together:

  • Initial page loads took longer, because styles were being initialised on the client
  • Server-side rendering performance declined, because style collection shifted from the client to the server
  • Updates to styles grew out of control as component count on a page grew

The stack was styled-components plus the sx prop, with styled-system removed at the end. The sx prop was genuinely useful, and the post is fair about that: excellent TypeScript support, direct integration with design tokens, and styles co-located with the component. The problem was the cost model — inline objects are inherently dynamic, and that dynamism had to be paid for on every render, on the server as well as the client.

Worth being precise about one thing: the post never uses the word hydration, and the 25% figure below is component initialisation time. If you see this migration described as a hydration fix, that is a secondary retelling.

Why CSS Modules won

The deciding argument was elimination of a runtime. CSS Modules let the team write native CSS in a file next to the JavaScript, keep the colocation and encapsulation they valued, scope class names locally by default, and drop all client and server runtime behaviour. Styles roll up into stylesheets that ship with the page HTML. There is no library computing styles at request time.

Two constraints shaped the choice as much as the performance did. Any alternative had to be deliverable component by component without breaking existing GitHub usage, and it had to keep the design system’s public API intact. Safety was a first-class requirement, not an afterthought.

The numbers, and the trap in them

Two headline figures circulate from this post, and they are frequently presented as contradicting each other. They do not.

FigureScopeWhenMetric
55%Primer design systemBy December 2024Less time to server-side render a page
25%Primer design systemBy December 2024Less time for components on a page to initialize
1%–22%Individual github.com services and controllersApril 2025 onwardServer-side rendering time improvement
All three figures are from the same post. The design-system numbers and the per-controller dotcom numbers measure different systems at different times.

The dotcom range comes from a rotation of eight engineers who migrated 6,419 sx props over six months. The prose states gains “ranging from 1% up to 22% in some pages.” The table embedded in the post gives a slightly different range, 3.05% to 21.97%, with the highest single value of 21.97% on the code_view commit controller. That internal discrepancy is in the original, so if you cite a number, cite the scope with it.

One more caution about the prop counts, because four different numbers appear and all of them are correct about something:

  • About 7,760 — peak backlog when the work started in April 2025
  • 6,419 — props migrated by the eight-engineer rotation over six months
  • 895 — what remained when the work resumed in April 2026
  • 0 — the end state, reached in three weeks by two engineers working with Copilot coding agents

Presenting 6,419 as “the total” is the most common error. It was not.

The migration method, component by component

Primer components had to keep shipping updates throughout, and those updates could not break existing usage in GitHub. So each component got the same four-step treatment:

  • Add a new file translating the existing styles into CSS Modules
  • Put the component behind a feature flag that toggles between the new and old styles
  • Use existing visual regression tests to verify the snapshots were identical between the two implementations
  • Roll the flag out gradually: own team, then GitHub staff, then all users

The visual regression step is what makes this safe rather than merely cautious. Two implementations of the same component rendering pixel-identically is a testable claim, and it converts a style migration into a series of small, verifiable substitutions.

The wrapper that made pausing possible

The post admits it undersold this part. Alongside the component migration, they built @primer/styled-react, a transitional package whose entire purpose was to let code still using sx import migrated components through the same API.

The mechanics are worth borrowing for any large API migration: consumers kept importing the component they already imported, while the implementation underneath switched to CSS Modules. Callers that had been rewritten could import straight from @primer/react and get the full performance win. The shim meant no consumer had to be rewritten in the same week its component changed underneath it.

Once enough call sites were converted, a “Styled Box Zero” phase cleared the remainder: translate remaining sx usage into equivalent CSS Module files, replace @primer/styled-react imports with @primer/react imports, test in pre-production, deploy.

Theming was the real blocker

With the sx migration finished, the expectation was that styled-components could be deleted. The post’s reaction is a single word: “Wrong!”

GitHub supports seven themes, each with a high-contrast variation, and all of it was enabled through styled-components. The colours themselves were not the problem — theming variables had always been defined in CSS, shipped via the @primer/css package, and Primer had already moved to non-styled theming in late 2025. What still needed removing was the JavaScript usage and the utilities that styled-components provided.

That became a third project, method unchanged: migrate, feature flag, roll out slowly, repeat. Two months after it started, the team was ready to remove the dependency — and they feature flagged the dependency removal itself, which is either admirable restraint or a good illustration of how much the flag infrastructure had become load-bearing. The post calls it “better safe than sorry.”

Automation helped. It did not do the job.

Three levels of automation were used in sequence: a VS Code extension for per-prop migration, an internal codemod for migrating whole files, and Copilot coding agents for the final push. GitHub released Copilot coding agent and Copilot code review while this work was still in progress, alongside other open-source agent tooling such as GitHub’s open-source AI fuzzing agent.

The honest framing from the post is that the work was “mostly automated” while still requiring “a bit of manual oversight and careful validation.” The most telling data point is the contrast in the last two phases: thousands of props took six months and eight engineers, then 895 took three weeks and two. The difference was not raw automation capability. It was that by the final phase the patterns were established, the edge cases were known, and the remaining work was genuinely repetitive.

That ratio is the part worth copying and the part worth being careful about. Bulk, agent-driven edits across a large repository are exactly the pattern where a wrong change reaches production without anyone reading it, which is the risk class described in how AI agents attack software supply chains. The 895-prop final phase was safe because the patterns were already known and reviewable, not because the agent was reliable.

What it cost

Three things are worth naming plainly, because they are the parts a planning document should include.

  • Scope expanded twice. A design system migration became a codebase-wide prop removal, which then became a theming decoupling. Each discovery arrived after the previous project was nominally complete.
  • It took about 13 months for the sx work, plus two more for theming. The design system itself was done by December 2024, so the dotcom work alone spans most of two years.
  • Momentum was lost and re-picked-up. The work paused and resumed around a year later, and the team explicitly notes the situation had changed by the time they returned to it.

Notably, the post contains no retrospective regret section and no “we would do this differently” paragraph. The closest admissions are the hedging about the wrapper library having been undersold, and the single “Wrong!”

What the post does not tell you

A few gaps are worth knowing if you are going to cite this work. Despite the title, there are no CSS payload or bundle-size figures anywhere in it — the argument is about where style computation happens, not how many bytes cross the network. No alternative approach is named and rejected; Tailwind, vanilla-extract, Emotion and Linaria are never discussed. And there is no Core Web Vitals outcome, which means the migration’s user-facing effect is not documented here.

Their summary is the most quotable line, and the most accurate: what looked at first like a CSS migration turned out to be a gradual re-platforming of how GitHub styles, themes and ships UI at scale. That is usually how these projects go. The visible deliverable is rarely the actual work.

References

Related Tools

Try these companion tools on DevStackIO Tools:

  • CSS formatter — format and clean up CSS stylesheets across multiple modules.