Read OSS

Material UI Internals: Architecture and the Package Dependency Pyramid

Intermediate

Prerequisites

  • React fundamentals (components, hooks, context)
  • Basic CSS-in-JS concepts (styled-components or Emotion patterns)
  • Familiarity with npm/pnpm and monorepo concepts

Material UI Internals: Architecture and the Package Dependency Pyramid

Material UI is one of the most widely used React component libraries in the world, yet few developers look beneath the surface of import Button from '@mui/material/Button'. This series changes that. Over six articles, we'll trace every line of consequence — from the moment your JSX invokes a component to the instant CSS hits the DOM. We start here, with the architecture that makes the whole system possible.

Material UI isn't a single package. It's a carefully layered monorepo where each package has a strict dependency contract with the layers beneath it. Understanding this pyramid is the key to understanding everything else in the codebase.

Monorepo Structure: pnpm Workspaces, Lerna, and Nx

The repository is organized around three workspace categories defined in the root workspace file:

pnpm-workspace.yaml

packages:
  - packages/*
  - packages/mui-envinfo/test
  - packages-internal/*
  - docs
  - test
  - test/*
Directory Purpose
packages/* Published npm packages (@mui/material, @mui/system, @mui/utils, etc.)
packages-internal/* Internal tooling not published to npm (markdown processing, docs utilities)
docs/ The Next.js documentation site at mui.com
test/ Cross-package integration tests

Three tools collaborate to manage this monorepo, each handling a distinct concern:

  • pnpm manages package installation and workspace linking. The workspace:^ protocol in package.json dependency fields tells pnpm to resolve packages from the local workspace rather than the registry.
  • Nx handles build caching and task orchestration. Its configuration is lean — just target defaults for build and copy-license tasks with cache outputs specified.
  • Lerna (historically) handles versioning and publishing to npm.

The Nx configuration at nx.json is notably minimal:

{
  "targetDefaults": {
    "build": {
      "cache": true,
      "dependsOn": ["copy-license", "^build"],
      "outputs": ["{projectRoot}/build", "{projectRoot}/dist", "{projectRoot}/.next"]
    }
  }
}

The "dependsOn": ["^build"] entry is critical: it means that before building any package, Nx first builds all of that package's dependencies. This ensures the four-layer pyramid builds bottom-up.

The Four-Layer Dependency Pyramid

Every component you render from @mui/material sits atop a precise dependency hierarchy. Understanding these layers is fundamental to the rest of this series.

graph BT
    L1_TYPES["@mui/types"]
    L1_UTILS["@mui/utils"]
    L1_THEMING["@mui/private-theming"]
    L2["@mui/styled-engine"]
    L3["@mui/system"]
    L4["@mui/material"]

    L1_TYPES --> L1_UTILS
    L1_UTILS --> L3
    L1_THEMING --> L3
    L2 --> L3
    L3 --> L4
    L1_UTILS --> L4

    style L4 fill:#1976d2,color:#fff
    style L3 fill:#42a5f5,color:#fff
    style L2 fill:#90caf9,color:#000
    style L1_TYPES fill:#bbdefb,color:#000
    style L1_UTILS fill:#bbdefb,color:#000
    style L1_THEMING fill:#bbdefb,color:#000

Layer 1 — Foundation: @mui/types provides shared TypeScript utilities. @mui/utils provides runtime helpers like deepmerge, composeClasses, generateUtilityClass, and formatMuiErrorMessage. @mui/private-theming provides the base ThemeContext and ThemeProvider.

Layer 2 — Styled Engine: @mui/styled-engine wraps Emotion's styled, css, and keyframes APIs behind a thin abstraction. This is the adapter layer that enables engine-swapping.

Layer 3 — Styling System: @mui/system provides createStyled (the factory function at the heart of all MUI styling), sx prop processing, theme construction, CSS variables support, and layout primitives like Box.

Layer 4 — Components: @mui/material is the component library itself — Button, TextField, Dialog, and 60+ more. Every component uses styled from Layer 3, which uses the engine from Layer 2.

You can verify this hierarchy directly in the package dependencies. In packages/mui-material/package.json:

"dependencies": {
    "@mui/system": "workspace:^",
    "@mui/types": "workspace:^",
    "@mui/utils": "workspace:^",
    ...
}

And in packages/mui-system/package.json:

"dependencies": {
    "@mui/private-theming": "workspace:^",
    "@mui/styled-engine": "workspace:^",
    "@mui/types": "workspace:^",
    "@mui/utils": "workspace:^",
    ...
}

Tip: When debugging a Material UI issue, identify which layer is involved first. A styling problem is likely in @mui/system (Layer 3), while a theme token issue could be in @mui/material/styles (Layer 4). The pyramid tells you where to look.

The Styled-Engine Abstraction

The styled-engine layer is elegant in its simplicity. The entire Emotion adapter is a single file — packages/mui-styled-engine/src/index.js:

import emStyled from '@emotion/styled';
import { serializeStyles as emSerializeStyles } from '@emotion/serialize';

export default function styled(tag, options) {
  const stylesFactory = emStyled(tag, options);
  // dev-mode validation omitted for clarity
  return stylesFactory;
}

export function internal_mutateStyles(tag, processor) {
  if (Array.isArray(tag.__emotion_styles)) {
    tag.__emotion_styles = processor(tag.__emotion_styles);
  }
}

export function internal_serializeStyles(styles) {
  wrapper[0] = styles;
  return emSerializeStyles(wrapper);
}

export { ThemeContext, keyframes, css } from '@emotion/react';

This file does three things: wraps Emotion's styled with dev-mode validation, exposes internal_mutateStyles for post-hoc style manipulation, and exposes internal_serializeStyles for pre-hashing CSS at definition time.

The styled-components alternative at packages/mui-styled-engine-sc/src/index.js provides the identical API surface but maps to styled-components internals:

flowchart LR
    SC["@mui/styled-engine-sc"]
    EM["@mui/styled-engine"]
    SYS["@mui/system"]

    SC -- "styled, keyframes, css" --> SYS
    EM -- "styled, keyframes, css" --> SYS

    SC2["styled-components"] --> SC
    EM2["@emotion/styled"] --> EM

The critical difference is in internal_serializeStyles. The Emotion adapter actually pre-serializes styles into hashed CSS strings. The styled-components adapter is a no-op — return styles — because styled-components doesn't expose an equivalent serialization API. This has performance implications we'll explore in Article 2.

Barrel Exports and the One-Level Import Rule

Material UI's public API is exposed through barrel files — index files that re-export from child modules. The main barrel at packages/mui-material/src/index.js follows a consistent pattern:

export { default as Button } from './Button';
export * from './Button';

export { default as TextField } from './TextField';
export * from './TextField';
// ...60+ more components

This pattern supports two import styles: import { Button } from '@mui/material' (barrel import) and import Button from '@mui/material/Button' (direct import). The ESLint configuration at eslint.config.mjs enforces a one-level import rule within the monorepo:

const OneLevelImportMessage = [
  'Prefer one level nested imports to avoid bundling everything in dev mode',
  'or breaking CJS/ESM split.',
].join('\n');

const NO_RESTRICTED_IMPORTS_PATTERNS_DEEPLY_NESTED = [
  {
    group: ['@mui/*/*/*'],
    message: OneLevelImportMessage,
  },
];

This means import Button from '@mui/material/Button' is acceptable, but import ButtonRoot from '@mui/material/Button/Button' is not. The rule exists for two reasons: it prevents deep imports from pulling in private implementation details, and it ensures bundler tree-shaking works correctly by maintaining a predictable module graph.

flowchart TD
    A["import Button from '@mui/material/Button'"] -->|"✅ One level"| B["packages/mui-material/src/Button/index.js"]
    B --> C["./Button.js (implementation)"]

    D["import Button from '@mui/material/Button/Button'"] -->|"❌ Two levels"| C
    E["import { Button } from '@mui/material'"] -->|"✅ Barrel"| F["packages/mui-material/src/index.js"]
    F --> B

Scoped Theming with THEME_ID

One of the most subtle architectural decisions in Material UI is the THEME_ID system. Every Material UI component extracts its theme not from the raw theme context, but from a scoped key within it.

The identifier is defined in packages/mui-material/src/styles/identifier.ts:

export default '$$material';

This ID is then passed to createStyled in packages/mui-material/src/styles/styled.js:

import createStyled from '@mui/system/createStyled';
import defaultTheme from './defaultTheme';
import THEME_ID from './identifier';
import rootShouldForwardProp from './rootShouldForwardProp';

const styled = createStyled({
  themeId: THEME_ID,
  defaultTheme,
  rootShouldForwardProp,
});

When a component renders, createStyled resolves the theme through attachTheme:

function attachTheme(props, themeId, defaultTheme) {
  props.theme = isObjectEmpty(props.theme)
    ? defaultTheme
    : props.theme[themeId] || props.theme;
}
sequenceDiagram
    participant Component as Button
    participant Styled as createStyled
    participant Theme as ThemeContext

    Component->>Styled: render with props
    Styled->>Theme: read theme from context
    Theme-->>Styled: { $$material: {...}, $$joy: {...} }
    Styled->>Styled: attachTheme(props, '$$material', default)
    Styled-->>Component: props.theme = theme['$$material']

This design enables a powerful use case: running Material UI and Joy UI on the same page with completely independent themes. Each library scopes to its own key within a shared theme context, so ThemeProvider can wrap both libraries without conflicts.

Tip: If you're building a component library on top of @mui/system, define your own THEME_ID (e.g., '$$mylib'). This ensures your library's theme doesn't collide with Material UI when consumers use both.

What's Next

With the architectural foundation established, we now understand where code lives and how packages relate. In Part 2, we'll dive into the single most important function in the entire codebase: createStyled. It's the factory function that transforms a simple styled(Button)({...}) call into a fully themed, variant-matched, sx-prop-enabled React component — and its three-zone expression pipeline is more intricate than you'd expect.