Installs into .claude/skills of the current project.
Are you the author of Web Ui Mui?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/agents-inc-web-ui-mui)
---
name: web-ui-mui
description: Material UI component library patterns for React
---
# MUI (Material UI) Patterns
> **Quick Guide:** MUI is a pre-styled React component library built on a theme. `createTheme` +
> `ThemeProvider` define the tokens every component reads; `sx` styles one instance, `styled()`
> makes a reusable one, `theme.components` changes every instance, and `slots` / `slotProps` reach
> inner elements. **Current: v7.x (March 2025)** — CSS layers, `slots`/`slotProps` standardised
> everywhere, Grid v2 promoted to `Grid`, React 19 compatible. MUI X v8 covers DataGrid, DatePicker
> and Charts. Emotion is the styling engine; Pigment CSS is still alpha and on hold.
**Detailed Resources:**
- [examples/core.md](examples/core.md) — theme with colour schemes, component overrides, dark-mode menu, TypeScript augmentation, SSR and SPA setup with CSS layers
- [examples/styling.md](examples/styling.md) — `styled()`, custom props via `shouldForwardProp`, `sx` theme-aware, responsive and callback forms
- [examples/form-inputs.md](examples/form-inputs.md) — TextField, Select, Autocomplete, slots and slotProps
- [examples/layout.md](examples/layout.md) — Grid, Stack, Box, Container, responsive card grid
- [examples/navigation.md](examples/navigation.md) — AppBar, Drawer, Tabs, responsive dashboard shell
- [examples/feedback.md](examples/feedback.md) — Dialog, Snackbar, Alert, Skeleton, CircularProgress
- [examples/data-grid.md](examples/data-grid.md) — DataGrid columns, pagination, cell rendering
- [reference.md](reference.md) — component tables, theme structure, `sx` shorthands, breakpoints, package list, v6 → v7 migration, anti-pattern code
---
<critical_requirements>
## Before writing MUI code
**Wrap the app in `ThemeProvider` with a `createTheme()` instance.** Without one, components fall
back to MUI's default theme and render correctly but wrong — no error, just someone else's design.
**Import from the path, not the barrel** — `@mui/material/Button`, not `{ Button } from
"@mui/material"`. The barrel makes the dev server parse the whole package on first load, and
`@mui/icons-material` alone is over 2000 modules.
**Use `slots` and `slotProps` to reach inner elements.** They are the v7 API across every component;
`components` and `componentsProps` are deprecated and scheduled for removal.
**Write dark-mode branches as `theme.applyStyles("dark", { … })`.** It emits both rulesets and lets
CSS pick, so the server and the client agree. `theme.palette.mode === "dark"` decides at render time,
which is what makes the wrong theme flash before hydration.
</critical_requirements>
---
**Auto-detection:** MUI, Material UI, @mui/material, @mui/system, @mui/icons-material,
@mui/x-data-grid, @mui/x-date-pickers, createTheme, ThemeProvider, CssBaseline, sx prop, styled,
useTheme, useColorScheme, colorSchemes, cssVariables, applyStyles, slots, slotProps,
shouldForwardProp, GridColDef, StyledEngineProvider, enableCssLayer
**Applies to:**
- Building on MUI's component set and customising it through the theme rather than by forking
- Choosing between `sx`, `styled()`, `theme.components` and `slots`/`slotProps` for a given override
- Colour schemes and dark mode, including the CSS-variables route that survives server rendering
- Typing custom palette colours and typography variants through module augmentation
- MUI X — DataGrid, date pickers, charts
**Handled elsewhere:**
- CSS methodology and design-token architecture beyond MUI's own theme — `enableCssLayer` puts MUI's
styles in a named layer so an external stylesheet can order itself against them, and how that
stylesheet is written is settled elsewhere
- Unstyled primitives — this library arrives fully styled, and a project that wants to own every
pixel wants headless primitives instead
- Form state and validation — `TextField` takes `error` and `helperText` to render a failure, and
`inputRef` to hand the underlying `<input>` to a library that registers by ref; what decides those
values is not this skill's concern
- Server state — components take data as props
---
<philosophy>
**The theme is the product.** Palette, typography, spacing, shadows, breakpoints, z-index and
transitions are all one object, and every component reads from it. An override written at a call
site is a decision that will need repeating; the same decision in `theme.components` is made once.
**Customisation is layered, and the layer is the choice.** `sx` for this instance, `styled()` for a
reusable variant, `theme.components` for every instance, `slots`/`slotProps` for elements inside a
component you do not own. Reaching for a heavier layer than the situation needs is the usual source
of style that cannot be changed later.
**MUI components are client components.** They use context and effects, so a server-rendered app
needs a cache provider above `ThemeProvider` and a client boundary around the pages that use them.
</philosophy>
---
<decision_framework>
## Which styling layer
```
Used in one place → sx prop
Reused, same shape each time → styled()
Every instance in the app → theme.components.MuiX
An element inside the component → slots / slotProps
```
## Which layout component
```
One axis, even spacing → Stack
Twelve-column grid → Grid (size prop)
Centred page with a max width → Container
Anything else that needs sx → Box
```
## Colour scheme
```
No dark mode → one palette in createTheme
Follow the system only → cssVariables: true
User can choose → colorSchemes + useColorScheme
Both, without a flash → cssVariables: { colorSchemeSelector: "data" } + useColorScheme
```
## MUI X data display
```
Under ~100 rows, read-only → Table
Sorting, filtering, pagination → DataGrid
Very large datasets → DataGridPro (row virtualisation)
Date or time entry → DatePicker / DateTimePicker
Charts → MUI X Charts
Hierarchy → TreeView
```
</decision_framework>
---
<patterns>
## Core patterns
### Pattern 1: Theme and provider
```tsx
const theme = createTheme({
palette: { primary: { main: "#1976d2" } },
typography: {
fontFamily: '"Inter", sans-serif',
button: { textTransform: "none" },
},
shape: { borderRadius: 8 },
spacing: 8,
});
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>;
```
`CssBaseline` applies MUI's normalisation and the theme's background and text colours to `<body>` —
without it the page around the components keeps the browser defaults.
Full code: [examples/core.md](examples/core.md)
---
### Pattern 2: CSS variables and dark mode
```tsx
const theme = createTheme({
cssVariables: { colorSchemeSelector: "data" },
colorSchemes: {
light: { palette: { primary: { main: "#1976d2" } } },
dark: { palette: { primary: { main: "#90caf9" } } },
},
});
const { mode, setMode } = useColorScheme(); // "light" | "dark" | "system"
```
With `cssVariables`, both palettes are emitted as custom properties and a `data-*` attribute selects
between them, so the scheme is settled before React runs. `mode` is `undefined` on the first render —
guard on it before rendering anything that depends on the scheme.
Full code: [examples/core.md](examples/core.md)
---
### Pattern 3: The `sx` prop
```tsx
<Box
sx={{
p: 3, // theme.spacing(3)
bgcolor: "background.paper", // theme.palette.background.paper
borderRadius: 1, // theme.shape.borderRadius
boxShadow: 3, // theme.shadows[3]
width: { xs: "100%", md: "50%" },
"&:hover": { boxShadow: 6 },
}}
/>
```
Numbers go through the theme's scales and dotted strings resolve against the palette, so a themed
value never needs to be repeated as a literal. Breakpoint objects replace media queries, and an
array of `sx` objects merges left to right — which is how a conditional style is added without
rebuilding the object.
Full code: [examples/styling.md](examples/styling.md)
---
### Pattern 4: `styled()`
```tsx
const StyledCard = styled(Card)(({ theme }) => ({
padding: theme.spacing(3),
"&:hover": { boxShadow: theme.shadows[8] },
...theme.applyStyles("dark", { backgroundColor: theme.palette.grey[900] }),
}));
```
Reach for this when the same styling appears more than once. A second argument takes
`shouldForwardProp`, which is what stops a styling-only prop reaching the DOM as an unknown
attribute.
Full code: [examples/styling.md](examples/styling.md)
---
### Pattern 5: Slots and slotProps
```tsx
<Autocomplete
slots={{ paper: CustomPaper }}
slotProps={{
paper: { elevation: 8, sx: { borderRadius: 2 } },
listbox: { sx: { maxHeight: 300 } },
input: ({ open }) => ({
sx: { borderColor: open ? "primary.main" : "divider" },
}),
}}
renderInput={(params) => <TextField {...params} label="Framework" />}
/>
```
`slots` replaces an inner component, `slotProps` configures one, and a slotProp written as a callback
receives that slot's own state. Define slot components outside the render — an inline arrow is a new
component type each time, which remounts the slot on every render.
Full code: [examples/form-inputs.md](examples/form-inputs.md)
---
### Pattern 6: Layout
```tsx
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 4 }}><Sidebar /></Grid>
<Grid size={{ xs: 12, md: 8 }}><MainContent /></Grid>
</Grid>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<Button variant="contained">Save</Button>
</Stack>
<Container maxWidth="lg" sx={{ py: 4 }}>{children}</Container>
```
In v7 `Grid` is the former Grid2 and takes one `size` prop instead of separate `xs`/`sm`/`md` props;
the previous component is still available as `GridLegacy`.
Full code: [examples/layout.md](examples/layout.md)
---
### Pattern 7: TypeScript augmentation
```ts
declare module "@mui/material/styles" {
interface Palette {
neutral: Palette["primary"];
}
interface PaletteOptions {
neutral?: PaletteOptions["primary"];
}
}
declare module "@mui/material/Button" {
interface ButtonPropsColorOverrides {
neutral: true;
}
}
```
Two augmentations, and both are needed: the first makes the token exist on the theme, the second
lets a component accept it as a prop value. Skip the second and `color="neutral"` is a type error at
every call site even though the theme carries it.
Full code: [examples/core.md](examples/core.md)
---
### Pattern 8: Server-rendered setup
```tsx
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<GlobalStyles styles="@layer theme, base, mui, components, utilities;" />
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</AppRouterCacheProvider>
```
Emotion generates styles as components render, so a server-rendered app needs a cache provider above
`ThemeProvider` to collect and flush them with the streamed HTML — without it the markup arrives
unstyled and restyles on hydration. MUI publishes an adapter package per supported server framework;
a client-only app uses `StyledEngineProvider` in the same position instead. `enableCssLayer` puts
MUI's output into a named layer, and the `@layer` declaration is what fixes the order of that layer
against everything else on the page.
Full code, both arrangements: [examples/core.md](examples/core.md)
</patterns>
---
<performance>
## Performance
**Path imports.** `import Button from "@mui/material/Button"` skips barrel parsing; the barrel form
costs several times the dev-server startup. Some bundlers and frameworks rewrite barrel imports
automatically — check before assuming the cost applies. A lint rule restricting the pattern
`^@mui/[^/]+$` is what keeps it from creeping back.
**Stable references for anything a component treats as identity.** DataGrid `columns`, slot
components, and `Dialog` `TransitionProps` are all compared by reference: define them at module
scope, or memoise them. Inline, they cause a re-render or a remount on every parent render.
**`createTheme` does not affect bundle size** — it is data, not components. The bundle is decided by
which components are imported.
</performance>
---
<red_flags>
## Red flags
**Breaks at runtime:**
- `theme.palette.mode === "dark"` in a style function — resolved at render time, so the server and
the first client paint disagree and the wrong theme flashes
- An inline arrow in `slots` — a new component type each render, so the slot remounts and loses its
state and focus
- Inline `columns` on a DataGrid — a new array each render, re-rendering the whole grid
- Importing more than one level deep, `@mui/material/styles/createTheme` — the v7 ESM package layout
does not expose those paths
- `@mui/lab` imports for Alert, Skeleton or Autocomplete — they graduated to `@mui/material` in v7
- MUI components rendered on the server without a client boundary — they need context and effects,
and the boundary belongs on the page, not only on the root layout
**Surprising behaviour:**
- `spacing` is a multiplier, not pixels: `spacing(2)` is 16px at the default base of 8
- `sx` arrays merge left to right, so a later entry wins — which is what makes conditional styles work
- `TextField` is three components in a trench coat (input, label, helper text), so targeting the
actual `<input>` means `slotProps.input`
- Setting `zIndex` by hand fights MUI's own scale, where modal, drawer, snackbar and tooltip already
have assigned values
- `useMediaQuery` returns `false` during server rendering, so a desktop-first branch renders the
wrong thing before hydration
- `useColorScheme().mode` is `undefined` on the first render, and rendering a scheme-dependent icon
from it produces a hydration mismatch
- A missing TypeScript augmentation for a custom palette colour is a type error at the usage site,
not at the theme
- `components` and `componentsProps` still work in v7 but are deprecated
</red_flags>