← All pages Practise what we preach. View this page in:
Vefskólinn · Component libraries

Carbon and Material UI

The same ideas as on the Hanna and Ísland.is page, in two design systems that are used as examples all over the world. Carbon is IBM's design system. Material UI (MUI) is the most used React library built on Google's Material Design. Like the Icelandic pair, they make opposite choices: Carbon starts from CSS, MUI starts from React.

5.5 million weekly downloads of @mui/material 83,000 weekly downloads of @carbon/react npm, week to 20 October 2026
Carbon · IBM The styles are the foundation.

Carbon's CSS is written in Sass and shared by its React library and its Web Components library. The React components mostly choose class names.

Storybook: react.carbondesignsystem.com ↗
Material UI · built on Google's Material Design The React component is the product.

Styles are written as JavaScript objects and turned into CSS in the browser while the page runs. MUI is its own company. Google designs Material, MUI builds it for React.

Docs: mui.com/material-ui ↗
Part 1

The problems it solves

The same problems you know from group projects, at the scale of companies with hundreds of developers.

Dividing the work

Split a big product into small parts with clear borders. Then many people can work at the same time without getting in each other's way.

Carbon

One repository, split into packages that each have one job:

packages/
  colors/          the colour palette
  themes/          tokens built from the colours
  type/  grid/  layout/  motion/  icons/
  styles/          the CSS for every component
  react/           React components
  web-components/  the same components as Web Components

Even one Button is split by job. The CSS lives in styles/scss/components/button/ and the React part in react/src/components/Button/, next to Button.stories.js (Storybook), Button.mdx (docs) and a __tests__ folder.

github.com/carbon-design-system/carbon ↗
Material UI

Every component is one folder in packages/mui-material/src/:

  • Button/Button.jsthe component and its styles
  • Button/Button.d.tsthe TypeScript types
  • Button/buttonClasses.tsthe readable class names
  • Button/Button.test.jstests

Here the styles sit in the same file as the component. Splitting by component, with everything for one component in one place, is the same idea as Ísland.is.

github.com/mui/material-ui ↗

In your project: both systems give every component an owner and a home. The same rule works for a team of four: one owner per component, and everything for that component in one folder.

Where to start, one piece at a time

Build from the bottom up. First the tokens, then the smallest components, then bigger components made from the small ones.

Carbon
@carbon/colors   blue60 = #0f62fe
     ↓
@carbon/themes   button-primary = blue60
     ↓
@carbon/styles   .cds--btn--primary { … }
     ↓
@carbon/react    <Button kind="primary">

The packages depend on each other in this order. You can see it in @carbon/styles' own list of dependencies: colors, themes, type, grid, layout and motion all come first.

Material UI
// packages/mui-material/src/Button/Button.js
import ButtonBase from '../ButtonBase'
import CircularProgress from '../CircularProgress'

ButtonBase handles the behaviour every clickable thing needs: focus, keyboard and the ripple effect. Button adds the Material look on top. CircularProgress is the spinner a button shows while it's loading. Under it all is the default theme from createTheme(): colours, type, an 8px spacing unit and breakpoints.

In your project: notice that MUI separates behaviour (ButtonBase) from looks (Button). If your project has several kinds of buttons, build the shared behaviour once.

Consistency: change it in one place

Every component takes its colours from the same tokens, so the product looks consistent and one change updates everything that uses a token. At this scale the same idea also gives you whole themes.

Carbon
/* compiled Carbon CSS */
.cds--white { --cds-background: #ffffff; --cds-button-primary: #0f62fe; … }
.cds--g100  { --cds-background: #161616; --cds-button-primary: #0f62fe; … }

.cds--btn--primary {
  background-color: var(--cds-button-primary, #0f62fe);
}

Carbon has four themes: White, Gray 10, Gray 90 and Gray 100. Each theme is one class that sets about 345 tokens. Wrap part of a page in <Theme theme="g100"> and everything inside it switches.

Material UI
const theme = createTheme({
  palette: {
    mode: 'light',
    primary: { main: '#1976d2' },
  },
})

<ThemeProvider theme={theme}>
  <App />
</ThemeProvider>

The theme is a JavaScript object. Every MUI component inside ThemeProvider reads its colours from it. #1976d2 is MUI's default primary blue.

Try it: change the theme

Both systems change in one place, but in different ways. On the left is Carbon's real CSS. On the right is the real MUI library, running in your browser.

Carbon: swap a class
@carbon/styles 1.118 · button + themes
CSS onlyThe browser swaps one class. No JavaScript styling.
<div class="cds--white">
Material UI: a new theme object
@mui/material 5.16 · React 18 · Emotion
LoadingFetching React and MUI from a CDN…
<button class="… css-?">

The demo uses MUI 5, the last version with a build that runs straight in the browser. MUI 9 still uses Emotion to style components by default.

In your project: if you plan a dark mode, use tokens from day one. Swapping a theme is easy when every colour already has a name, and very hard when colours are typed into fifty files.

Part 2

The programming ideas underneath

General programming ideas, made visible. These two systems show them at a scale where getting them wrong would cost thousands of developers time.

Abstraction

Hiding the details behind a simple surface, so you can use something without knowing how it works inside.

Carbon What you write
<Button kind="primary">Submit application</Button>
What's hidden underneath
// packages/react/src/components/Button/ButtonBase.tsx (shortened)
const prefix = usePrefix();
const buttonClasses = classNames(className, {
  [`${prefix}--btn`]: true,
  [`${prefix}--btn--${kind}`]: kind,
  [`${prefix}--btn--disabled`]: disabled,
});
What the browser gets
<button class="cds--btn cds--btn--primary" type="button">

The React component's main job is to pick class names. The CSS comes separately from @carbon/styles. The cds prefix comes from usePrefix(), so a company can change it if it clashes with other CSS.

Material UI What you write
<Button variant="contained">Submit application</Button>
What's hidden underneath
// packages/mui-material/src/Button/Button.js (shortened)
const ButtonRoot = styled(ButtonBase, {
  name: 'MuiButton',
  slot: 'Root',
})(({ theme }) => ({
  ...theme.typography.button,
  minWidth: 64,
  padding: '6px 16px',
  borderRadius: theme.shape.borderRadius,
}))
What the browser gets
<button class="MuiButtonBase-root MuiButton-root
  MuiButton-contained MuiButton-colorPrimary css-1s9xu08">

styled() turns a JavaScript object into CSS while the page runs, using a library called Emotion. The hashed class css-1s9xu08 holds the styles. The readable MuiButton-root names are added so you can still target the button from your own CSS.

Both class lists above come from real test builds of the libraries, made in Vite with React 19 on 21 October 2026. Your hash may differ.

In your project: give your components a small, clear set of props, like kind or variant. Whoever uses the component chooses from those, and the details stay inside.

Modularity

The system is made of separate modules with clear borders, and you take only the ones you need.

Carbon
// everything
@use '@carbon/react';

// only what a page with buttons needs
@use '@carbon/react/scss/reset';
@use '@carbon/react/scss/zone';
@use '@carbon/react/scss/components/button';
Everything1,227 KB Only buttons130 KB

The same page, built twice with Vite. The CSS file is almost ten times smaller when you only take the modules you use.

Material UI
// both work
import { Button } from '@mui/material'
import Button from '@mui/material/Button'

You import components one by one. There is no CSS file to trim, because Emotion only creates CSS for components that actually appear on the page. In our test build, npm run build produced no CSS file at all, only one 342 KB JavaScript file (React included).

In your project: modularity has a size you can measure. Run npm run build and look at what lands in dist/. That's what your users download.

Dependencies

Everything a module needs in order to work. At this scale, dependencies are mostly well managed. But they still break, and some of them do things you didn't ask for.

Carbon A tool you install yourself

Carbon's styles are Sass, so sass is a peer dependency: you have to install it in your own project. On 21 October 2026, Sass 1.106.0 came out. Our test build of Carbon failed with it:

$ npm run build
✗ Build failed in 6.14s
[plugin vite:css] src/index.scss
RolldownError: [sass] undefined

$ npm install -D sass@1.105.2
$ npm run build
✓ built

Nothing in Carbon changed. A new version of a dependency did, and the old version still worked.

A dependency that collects data

@carbon/react and @carbon/styles depend on @ibm/telemetry-js. It collects anonymous usage data, but only on CI servers like GitHub Actions, never on your own computer. You can turn it off by setting IBM_TELEMETRY_DISABLED=true.

Material UI A styling engine you install yourself
npm install @mui/material @emotion/react @emotion/styled

MUI's styles depend on Emotion, and Emotion is a peer dependency too. MUI accepts React 17 to 19.

A dependency on JavaScript itself

Because Emotion creates CSS while the page runs, the styles need JavaScript running in the browser. React Server Components, which render on the server without browser JavaScript, can't do that. This is one reason the MUI team started Pigment CSS, which creates the CSS at build time instead. It's still experimental, at version 0.0.31.

When a library is retired. Shopify's Polaris was one of the best-known React design systems. Today npm shows this when you install it: “Polaris React is deprecated and no longer maintained. For building Shopify admin experiences, use Polaris web components.” Google's own Material Web library says it is “in maintenance mode pending new maintainers”. Every app built on a library depends on the people maintaining it.

In your project: before you install a library, check three things on npm: when it was last published, which versions of React it accepts, and what it depends on. And commit your package-lock.json, so everyone in the group gets the same versions.

Decoupling

Keeping parts independent of each other, so you can change or replace one without breaking the others.

Carbon
// React, with @carbon/react
<Button kind="primary">Submit application</Button>

<!-- Web Components, with @carbon/web-components -->
<cds-button kind="primary">Submit application</cds-button>

Both libraries depend on the same @carbon/styles. The styles don't know which framework is using them, so the button looks the same in React, in Web Components, or in plain HTML with the class names.

Material UI

Three kinds of decoupling:

  • Design ↔ codeGoogle writes the Material Design guidelines. MUI, Angular Material, Flutter and Google's own Material Web all build from them.
  • Component ↔ styling engineMUI's components call MUI's own styled(), not Emotion directly. The repo has packages for using styled-components or Pigment CSS instead.
  • Behaviour ↔ looksThe MUI team also builds Base UI, components with behaviour and accessibility but no styles at all.

In your project: put a thin layer of your own between your code and the libraries you use. If every component imports your Button, and only your Button imports the library, you can switch libraries by changing one file.

Part 3

Why people argue about it

With the Icelandic page, you have now seen three ways to style components. The big libraries are slowly moving toward the same answer.

Runtime or build time

Plain CSS with variables (Hanna, Carbon), CSS generated at build time (Ísland.is), or CSS generated by JavaScript while the page runs (MUI). Each is a position in the long argument about CSS-in-JS.

Carbon · Sass, then CSS variables

Sass runs at build time and produces plain CSS. The tokens stay as CSS variables, so themes still switch in the browser.

  • Themes switch without JavaScript. You did it in the demo above.
  • Readable class names in DevTools.
  • Works with any framework.
  • Class names are global. The cds-- prefix is there to avoid clashes.
  • You need Sass in your build, with a version that works.
Material UI · Emotion, at runtime

JavaScript creates the CSS in the browser. Styles can react to props and the theme directly, for example through the sx prop.

  • Styles can depend on anything JavaScript knows.
  • Only CSS for components on the page is created.
  • Hashed class names never clash.
  • The browser does extra work every time styles change. Watch the rule count in the demo.
  • Doesn't work with React Server Components.

MUI has added a cssVariables option. With it turned on, our test build put the theme in CSS variables, and the button read its colour like this:

--mui-palette-primary-main: #1976d2;
--variant-containedBg: var(--mui-palette-primary-main);

In your project: that's the same idea Hanna and Carbon use. Whatever tool you choose, putting your tokens in CSS variables is a safe bet.

Specific to your product

A design system holds what's only true for one product. A general library has to work for everyone, so it leaves that part to you.

Carbon · made for IBM
  • IBM PlexIBM's own typeface, a dependency of @carbon/styles
  • Productive / expressivetwo styles: compact for work apps, larger for marketing pages. The Button has an isExpressive prop.
  • AI labela component and --cds-ai-aura-* tokens for marking AI-generated content in IBM's AI products
Material UI · made for everyone

MUI's defaults follow Google's Material Design. If you don't change the theme, your app looks like a Google app, and like every other MUI app. That's why MUI's docs have a whole section on theming. Making it yours is your job.

The same goes for AI tools. Ask for a button and you get a generic one, often MUI-flavoured. One popular library, shadcn/ui, takes the opposite approach to MUI: you don't install it as a dependency, you copy its components into your project and change them to fit your product.

In your project: list what's specific to your product before you choose a library. If the list is long, a library you customise heavily, or your own components, may be less work than fighting a generic one.

Try it yourself

Exercises

Both libraries install with a normal Vite and React 19 project. Every step below was tested on 21 October 2026. Exercise 3 is a few questions to think about, alone or with others.

Time: 20–30 minYou need: Node 22 or newer, npmGoal: see themes, tokens and modules at work
  1. Create a Vite project and install Carbon

    npm create vite@latest carbon-try -- --template react-ts --no-interactive
    cd carbon-try
    npm install
    npm install @carbon/react
    npm install -D sass@1.105.2
    We pin Sass to 1.105.2 because 1.106.0 fails to build Carbon's styles (see Dependencies). If a newer Sass works for you, great. Try it without the version first if you're curious.
  2. Load Carbon's styles

    Create src/index.scss. Then delete src/App.css and src/index.css, and in src/main.tsx replace import './index.css' with import './index.scss'.

    src/index.scss
    @use '@carbon/react';
  3. Use the components

    Replace all of App.tsx. The second row is wrapped in a Gray 100 theme.

    src/App.tsx
    import { Button, Theme } from '@carbon/react'
    
    export default function App() {
      return (
        <main style={{ padding: 32, display: 'grid', gap: 24 }}>
          <div style={{ display: 'flex', gap: 16 }}>
            <Button>Submit application</Button>
            <Button kind="secondary">Cancel</Button>
            <Button kind="danger">Delete</Button>
          </div>
          <Theme theme="g100">
            <div style={{ padding: 24, display: 'flex', gap: 16 }}>
              <Button>Submit application</Button>
              <Button kind="tertiary">Cancel</Button>
            </div>
          </Theme>
        </main>
      )
    }
  4. Run it and inspect

    npm run dev

    You should see: three buttons on white, and a dark strip with two more. In DevTools, the buttons have class="cds--btn cds--btn--primary", and the dark strip is a <div class="cds--g100 cds--layer-one">. Click it and look at all the --cds- variables it sets.

  5. Override a token

    Add this to the end of src/index.scss:

    src/index.scss
    :root {
      --cds-button-primary: #7c3aed;
    }

    You should see: the top primary button turns purple, but the one in the dark strip stays blue. The cds--g100 div sets its own --cds-button-primary, and the closest definition wins. To change both, add the same line inside a .cds--g100 { } rule as well.

  6. Measure modularity

    Run npm run build and note the size of the CSS file in dist/assets. Then replace the first line of index.scss with these three, and build again:

    src/index.scss
    @use '@carbon/react/scss/reset';
    @use '@carbon/react/scss/zone';
    @use '@carbon/react/scss/components/button';

    You should see the CSS shrink from about 1.2 MB to about 130 KB, with the page looking the same.