Wendeson Gomes
← Back to blogDesign systems

Supporting 3 design systems without scattering ifs through your code

Oct 29, 20266 min readby Wendeson Gomes

One product can wear three different faces. Each environment picks its own design system, such as Material (Google), Fluent (Microsoft) or Bootstrap. The screen is the same. The button, the field and the dialog change.

The naive path is what everyone tries first: an if scattered through the screens, something like "if Material, render this button, else that one". It works the first week. By the tenth, every screen knows three worlds and any visual tweak turns into an if hunt.

The rule that held everything together was simple: apps and modules don't know which design system is active. They ask for a Button, a Combobox, a Dialog. A UI package decides how that looks, and the difference between systems becomes an override in there, never an isMaterial() in a screen. Switch the design system below: the screen code doesn't change a line.

UI package

<Button />decides which one to draw

the client sees their theme

New request

Maria Souza
Name
Purchase approval▾
Process
CANCELSAVE
the screen code is the same in all threematerial
<Field label={t('name')} />
<Combobox label={t('process')} />
<Checkbox>{t('notify')}</Checkbox>
<Button>{t('save')}</Button>

Tap to switch. The screen doesn't change a line.

What the package solves once

  • Only the active one downloadsEach system lives in its own module, loaded with import(). Whoever uses Material never downloads Fluent or Bootstrap.
  • Accessibility in the componentVisible focus, contrast, keyboard, ARIA roles and prefers-reduced-motion live in the package, not in every screen.
  • Theme, dark and RTL through tokensSwitching system, mode or direction means switching values, with no if in a screen.

Active design system: Material. Only it was loaded.

Three habits help keep that boundary. No new colors: everything comes from CSS custom property tokens, so switching theme means switching values, not classes. If only one screen breaks in one system, I fix the screen, not the shared component, because changing the shared one to please a single consumer breaks the other two systems. And every z-index stays contained, because three libraries stacking layers is a recipe for a modal behind another.

Another point that tends to be forgotten is weight. Each design system lives in its own module and loads on demand, through a dynamic import. If the environment uses Material, the browser never downloads the code of the other two. Without that, supporting three systems would mean shipping all three to everyone.

Accessibility also lives in the package, not in the screens. Visible focus, enough contrast, keyboard navigation, correct ARIA roles and respect for prefers-reduced-motion are solved once, inside the component. Since theme, dark mode and direction arrive through the same place, they all inherit the same care.

Even with the boundary right, every library has its quirks. Three that cost me time: react-hook-form's register doesn't write values into components that aren't native inputs, you need Controller; padding passed via className falls outside the border when the library draws inside a shadow DOM; and a headless lib (like Ark UI) and MUI fight over the focus trap when one dialog opens inside the other.

Dark mode and Arabic (RTL) go through the same math: they become one more axis inside the UI package, not one more if in a screen. Hit Dark and RTL in the demo to see it.

The boundary between what the screen wants and how the system draws it is what pays the bill for having three themes.
Next up
Making my portfolio Google-ready
→