MUI component styling and implementation rules — sx prop patterns, theme usage, dark mode, spacing, accessibility, form best practices, chart config, and component-specific gotchas. Use whenever building or modifying MUI components, reviewing MUI code, or implementing designs with Material UI. Triggers on any task involving MUI component creation, styling, theming, or mockup implementation.
Staff Design Engineer with comprehensive MUI expertise and pixel-perfect implementation skills.
theme.applyStyles('dark', styles) exclusivelyonClear when onChange(null) suffices)gap at least 1 unless design explicitly says otherwise<Box component="img" /> with empty src and proper alt, style via sx with proper aspectRatiohttps://placehold.co/600x400 (no query params). Use correct aspect ratio (e.g., 3:4 → https://placehold.co/600x400, square → https://placehold.co/400)maxWidth/width — let them flow naturally; control width from the preview pageSemantic text (error, success, info, warning): use <palette>.text token for better contrast
<Typography sx={{ color: "error.text" }}>Error</Typography>
<Box sx={theme => ({
color: (theme.vars || theme).palette.success.text,
})}>
High contrast background → Button with custom borderRadius (IconButton doesn't support variant)
<Button variant="contained" sx={{ borderRadius: 99 }}>
<AddIcon />
</Button>
IconButton only for secondary actions or lists of same-size icon-only buttons
No textTransform: "none" needed — built-in theme already handles it
Don't customize buttons with grey tokens — use primary color
Always start with zero margin/axis, then adjust:
import { BarChart } from '@mui/x-charts/BarChart';
<BarChart
margin={{ left: 0, right: 0, top: 0, bottom: 0 }}
xAxis={[{ height: 0, position: 'none' }]} // min 28 to display label
yAxis={[{ width: 0, position: 'none' }]} // min 28 to display label
/>;
slotProps.legend.sx.display = "none"valueFormatter: (params) => \${params.value}%``colors prop with string arraymargin pattern as above<Chip variant="filled" color="success|error|info|warning|secondary">@mui/icons-material. Fallback: lucide-react<Box sx={{ display: 'inline-block', width: size, height: size, bgcolor: 'text.icon', borderRadius: '50%' }} />sx={{ alignItems: 'flex-start' }} — NOT the alignItems prop
ListItemAvatar for alignment (unless ListItemText is not used)disablePadding when secondaryAction is present (removes padding-right)secondaryAction: ensure padding-right accommodates the action contentslotProps.secondary.component to "div" if secondary is a React element (avoids <p> nesting)h5/h6 variants — lowest heading is h4label prop — not separate TypographyslotProps — not deprecated InputProps/InputLabelProps
slotProps.input, slotProps.inputLabel, slotProps.htmlInputrequired, error, helperText for validation and a11y// ✅ CORRECT
<TextField
fullWidth
required
label="Card Number"
placeholder="1234 5678 9012 3456"
variant="outlined"
value={formData.cardNumber}
onChange={handleInputChange("cardNumber")}
error={!!errors.cardNumber}
helperText={errors.cardNumber || "Enter 16-digit card number"}
/>
// ❌ INCORRECT
<Box>
<Typography variant="body2">CARD NUMBER</Typography>
<TextField
fullWidth
placeholder="1234..."
InputProps={{ /* deprecated */ }}
/>
</Box>
sx Prop Rulesheight — let padding/line-height determine it- sx={theme => ({ borderRadius: (theme.vars || theme).shape.borderRadius * 3 })}
+ sx={{ borderRadius: 3 }}
- sx={theme => ({ color: (theme.vars || theme).palette.primary.main })}
+ sx={{ color: "primary.main" }}
Use callback as value or array item. NEVER spread callback in object:
// ✅ Callback as value
sx={theme => ({
color: (theme.vars || theme).palette.primary.main,
})}
// ✅ Callback as array item
sx={[
{ borderRadius: 2 },
theme => ({
color: (theme.vars || theme).palette.primary.main,
})
]}
// ❌ NEVER — callback spread in object
sx={{
borderRadius: 2,
...theme => ({
color: (theme.vars || theme).palette.primary.main,
})
}}
sx propsAlways use array syntax:
function MyButton({ sx, ...props }: MyButtonProps) {
return (
<IconButton
sx={[
{ color: 'text.secondary', '&:hover': { color: 'text.primary' } },
...(Array.isArray(sx) ? sx : [sx]),
]}
{...props}
/>
);
}
Wrap in @media (hover: hover):
sx={theme => ({
bgcolor: "background.paper",
"@media (hover: hover)": {
"&:hover": { bgcolor: "action.hover" },
},
})}
sx={{ width: { xs: "100%", md: "50%" } }}theme.breakpoints.up("md")sx={theme => ({
width: "100%",
[theme.breakpoints.up("md")]: { width: "50%" },
})}
sx={theme => ({
[theme.containerQueries?.up("sm") || "@container (min-width: 600px)"]: {
gridColumn: "span 6",
},
[theme.containerQueries?.up("md") || "@container (min-width: 900px)"]: {
gridColumn: "span 7",
},
})}
Both container + media queries with class selectors:
sx={theme => ({
[theme.containerQueries?.up("md") || "@container (min-width: 900px)"]: {
width: "50%",
},
".responsive-media &": {
[theme.breakpoints.up("md")]: { width: "50%" },
},
})}
(theme.vars || theme).palette.* for palette/shape accesstheme.typography directly (NOT theme.vars.typography)// ✅ CORRECT
sx={{
borderRadius: 3,
color: "primary.main",
p: 2,
...theme.applyStyles('dark', { bgcolor: "grey.900" })
}}
// ❌ INCORRECT
sx={{
borderRadius: "12px",
color: "#1976d2",
padding: "16px",
bgcolor: isDarkMode ? "grey.900" : "white"
}}
useTheme() + isDarkMode patterntheme.applyStyles('dark', styles):// ✅ Correct
sx={theme => ({
bgcolor: "background.paper",
...theme.applyStyles('dark', { bgcolor: "grey.900" }),
})}
// ❌ Incorrect — callback spread in object
sx={{
bgcolor: "background.paper",
...theme => theme.applyStyles('dark', { bgcolor: "grey.900" }),
}}
aria-describedby for forms, aria-live for dynamic content)IconButton needs aria-label, don't wrap disabled buttons in Tooltip::after for click area extensionaria-live)