DEV Community

Cover image for How We Migrated 150+ Components from MUI to Carbon Design System v11 (And Reduced Accessibility Violations by 78%)
Thoithoi Shougrakpam
Thoithoi Shougrakpam

Posted on

How We Migrated 150+ Components from MUI to Carbon Design System v11 (And Reduced Accessibility Violations by 78%)

A practical account of a large-scale UI library migration — what we planned, what broke, and what we'd do differently.


When our team decided to migrate from Material UI to Carbon Design System v11, the scope was clear: 150+ components across an enterprise DNS platform, hundreds of thousands of lines of React code, and zero tolerance for regressions on production traffic.

What wasn't clear was how much the migration would teach us about the hidden cost of design system debt.

Here's what we learned.


Why We Left MUI

MUI is excellent. We're not here to trash it. But for a product shipping inside IBM's ecosystem, we were maintaining two parallel design languages — MUI's Material-influenced components and IBM's Carbon — in ways that created real friction:

  • Accessibility inconsistencies: MUI and Carbon have different ARIA patterns. Running them side-by-side meant our a11y audit results were impossible to reason about systematically.
  • Token misalignment: MUI's theming system uses its own token schema. Syncing it with Carbon's design tokens required manual overrides that drifted every major version.
  • Duplication: We had parallel implementations of the same UI patterns (modals, data tables, notifications) in both systems because some features were built before Carbon was standardized internally.

The business case was straightforward: standardize on one system, reduce the a11y violation count, and cut the long-term maintenance surface area.


What the Migration Actually Looked Like

Phase 1: Audit Before You Touch Anything

Before writing a single line of migration code, we did a full component inventory. Every MUI import, tagged by:

  • Component type (Button, Modal, DataTable, etc.)
  • Usage frequency across the codebase
  • Complexity (prop surface area, custom styling overrides)
  • WCAG failure count from our latest audit

This gave us a priority-ordered migration queue. We started with high-frequency, low-complexity components (buttons, form elements, notifications) and saved the complex ones (data tables, modals with deeply custom behavior) for last.

Lesson: Don't migrate in file order. Migrate in impact order. Fixing <Button> across 30 files in one PR is fast and momentum-building. Fixing a deeply customized <DataTable> is a sprint's worth of work.

Phase 2: Coexistence, Not Big Bang

We explicitly rejected a big-bang cutover. The goal was a migration where both systems could live together during the transition, which meant:

  1. No shared global styles between MUI and Carbon — we kept them scoped.
  2. A migration tracking doc that listed every component, its status (MUI / In Progress / Carbon), and the engineer assigned to it.
  3. Incremental PRs by page/feature area, not by component type — this kept feature work reviewable and deployable without blocking the whole migration.

The coexistence period lasted about three months. It wasn't elegant, but it worked.

Phase 3: Accessibility as a Migration Test

Here's where things got interesting.

Because Carbon is built with WCAG 2.1 AA compliance as a first-class constraint, migrating to Carbon components was — by construction — also an accessibility remediation pass. We set up automated a11y scanning (using axe-core in our Cypress suite) to run on every PR during the migration.

This gave us something useful: a per-PR accessibility delta. Each Carbon migration PR came with a number: how many violations were added or removed. Teams could see the a11y impact of their work in CI.

By the end of the migration, we had reduced our total WCAG violation count by 78%.

That number sounds impressive — and it is — but it's also a measure of how much debt had accumulated under our previous approach. The violations didn't appear overnight. They accumulated slowly, component by component, over years of building with a system that wasn't designed for our a11y requirements from the ground up.


What Broke (And How We Fixed It)

1. Theme Token Mismatches

Carbon v11 introduces a new token structure (using CSS custom properties instead of Sass variables). If you have any legacy Sass-based theming, this is a full rewrite, not a find-and-replace.

What we did: Audited every Sass file for hardcoded hex values or MUI theme variable references. Replaced them with Carbon's $layer-01, $text-primary, etc. tokens. This took longer than expected — hardcoded colors are invisible until you're looking for them.

2. Modal and Focus Trap Behavior

MUI's <Modal> and Carbon's <Modal> handle focus trapping differently. We had several flows where keyboard navigation broke silently — no visible error, just focus escaping to the wrong element.

What we did: Added keyboard navigation Cypress tests for every modal flow before migrating it. If a test passed on MUI and failed on Carbon, we knew exactly what to fix. If we hadn't instrumented this first, we'd have shipped broken focus behavior to users.

3. DataTable Complexity

Carbon's <DataTable> is a compound component with a very different API from MUI's <DataGrid>. We had 30+ table instances, many with custom sorting, filtering, and batch action logic baked in.

What we did: Built a single abstraction layer (<DnsDataTable>) that wrapped Carbon's DataTable and exposed only the props our platform actually needed. This gave us one migration target instead of 30, and it's been easier to maintain since.

4. Icon Library Differences

MUI uses @mui/icons-material. Carbon uses @carbon/icons-react. Not one-to-one. Some icons have no Carbon equivalent; some Carbon icons have different semantic names.

What we did: Created an icon mapping spreadsheet during the audit phase. For icons with no equivalent, we either used a close Carbon alternative or imported the SVG directly.


What We'd Do Differently

Start the a11y scanning earlier. We added automated a11y tests mid-migration. If we'd set them up before we started, we'd have had a cleaner baseline and caught more regressions in early PRs.

Automate the import codemod. For simple component swaps (e.g., import { Button } from '@mui/material'import { Button } from '@carbon/react'), a codemod would have saved significant time. We wrote a few but not systematically enough.

Treat the migration as a product feature, not a background task. The teams that moved fastest were the ones that dedicated sprint capacity explicitly to migration work. The teams that tried to do it "alongside" feature work consistently took 2x longer.


Results

After the migration was complete:

  • 78% reduction in WCAG accessibility violations
  • 150+ components migrated to Carbon v11
  • Design consistency across the platform — no more hybrid MUI/Carbon UIs
  • Reduced long-term maintenance surface — one design system to track for updates, security patches, and React compatibility

The migration also surfaced a pattern we hadn't anticipated: Carbon's enforced structure pushed us toward better component composition habits. When you can't just pile custom styles on top of an MUI component, you're forced to think more carefully about what your component actually needs to expose.

That's a side effect we'd take again.


Takeaways

If you're considering a similar migration:

  1. Audit before you migrate. Know your component inventory, complexity, and a11y debt before writing the first line.
  2. Coexistence over big bang. Ship incrementally. A broken design system migration kills trust faster than almost any other engineering failure.
  3. Use a11y scanning as your migration test suite. If you're moving to a more accessible system, instrument that signal from day one.
  4. Build abstraction layers for complex components. One internal DataTable wrapper is infinitely easier to migrate than 30 individual instances.
  5. Allocate sprint capacity. Migrations that happen "on the side" don't happen.

_Have questions about the migration approach or Carbon v11 specifics? Drop them in the comments — happy to go deeper on any section.

Top comments (0)