ui-engineer
Installation
SKILL.md
Role
Staff Design Engineer with comprehensive MUI expertise and pixel-perfect implementation skills.
Core Principles
- Minimal sx props — layout structure only, not decoration
- Theme-first — theme variables over hardcoded values
- Alias tokens only — never direct static tokens
- Dark mode:
theme.applyStyles('dark', styles)exclusively - TypeScript: no type errors on changed files
- Lean API: no redundant props (e.g., no
onClearwhenonChange(null)suffices)
Spacing
- 0.5 step increments only (0.5, 1, 1.5, 2...). Never arbitrary decimals like 1.2
- Text/icon spacing: 0.5–1.5 based on font size
- Component spacing: 1–2 based on component size
- Flexbox containers:
gapat least1unless design explicitly says otherwise
Images & Media
- Use
<Box component="img" />with emptysrcand properalt, style viasxwith properaspectRatio - Never use fake divs to simulate images
- Placeholders when no real image:
https://placehold.co/600x400(no query params). Use correct aspect ratio (e.g., 3:4 →https://placehold.co/600x400, square →https://placehold.co/400) - For components filling a layout (cards, buttons, inputs): don't set
maxWidth/width— let them flow naturally; control width from the preview page
Colors
-
Semantic text (
error,success,info,warning): use<palette>.texttoken for better contrast<Typography sx={{ color: "error.text" }}>Error</Typography> <Box sx={theme => ({ color: (theme.vars || theme).palette.success.text, })}>
Button vs IconButton
-
High contrast background →
Buttonwith customborderRadius(IconButton doesn't support variant)<Button variant="contained" sx={{ borderRadius: 99 }}> <AddIcon /> </Button> -
IconButtononly 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
greytokens — useprimarycolor
Charts
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
/>;
PieChart
- Hide legend:
slotProps.legend.sx.display = "none" - Format values:
valueFormatter: (params) => \${params.value}%`` - Arc colors:
colorsprop with string array - Remove spacing: same
marginpattern as above
Component-Specific Rules
Chip
- Subtle background →
<Chip variant="filled" color="success|error|info|warning|secondary">
Icon
- First:
@mui/icons-material. Fallback:lucide-react - Last resort:
<Box sx={{ display: 'inline-block', width: size, height: size, bgcolor: 'text.icon', borderRadius: '50%' }} />
ListItem
- Use
sx={{ alignItems: 'flex-start' }}— NOT thealignItemsprop- Add margin-top to
ListItemAvatarfor alignment (unlessListItemTextis not used)
- Add margin-top to
- Don't use
disablePaddingwhensecondaryActionis present (removes padding-right) - With
secondaryAction: ensure padding-right accommodates the action content
ListItemText
- Set
slotProps.secondary.componentto"div"ifsecondaryis a React element (avoids<p>nesting)
Typography
- No
h5/h6variants — lowest heading ish4
TextField & Forms
- Always use built-in
labelprop — not separate Typography - Use
slotProps— not deprecatedInputProps/InputLabelPropsslotProps.input,slotProps.inputLabel,slotProps.htmlInput
- Include
required,error,helperTextfor validation and a11y - Controlled components with proper state handling
- Clear errors on user interaction
// ✅ 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 Rules
- Minimize usage — layout structure, not decoration
- No hardcoded colors/spacing — use theme variables
- No explicit
height— let padding/line-height determine it - Use alias tokens:
- 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" }}
Theme access — MANDATORY
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,
})
}}
Merging sx props
Always use array syntax:
function MyButton({ sx, ...props }: MyButtonProps) {
return (
<IconButton
sx={[
{ color: 'text.secondary', '&:hover': { color: 'text.primary' } },
...(Array.isArray(sx) ? sx : [sx]),
]}
{...props}
/>
);
}
Hover on focusable elements
Wrap in @media (hover: hover):
sx={theme => ({
bgcolor: "background.paper",
"@media (hover: hover)": {
"&:hover": { bgcolor: "action.hover" },
},
})}
Responsive design
- Single field:
sx={{ width: { xs: "100%", md: "50%" } }} - Multiple fields:
theme.breakpoints.up("md")
sx={theme => ({
width: "100%",
[theme.breakpoints.up("md")]: { width: "50%" },
})}
Container queries
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 Usage
(theme.vars || theme).palette.*for palette/shape accesstheme.typographydirectly (NOTtheme.vars.typography)- No type errors after theme changes
// ✅ 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"
}}
Dark Mode
- Build as light mode even if mockup shows dark
- Never use
useTheme()+isDarkModepattern - Use
theme.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" }),
}}
Accessibility
MUI Accessibility Baseline
- MUI components include built-in keyboard navigation, focus management, and ARIA attributes
- Check MUI built-in a11y before adding custom ARIA — MUI follows WAI-ARIA practices by default
- Identify when additional ARIA is needed (
aria-describedbyfor forms,aria-livefor dynamic content) - Common gotchas:
IconButtonneedsaria-label, don't wrap disabled buttons inTooltip
Semantic Structure
- Card selections: RadioGroup/Radio (single), Checkbox/FormGroup (multi)
- Clickable cards: primary action on title with CSS
::afterfor click area extension - Navigation: use appropriate landmarks (AppBar, Drawer)
Keyboard & Screen Reader
- Logical tab order and focus indicators
- Focus trapping for modals/overlays
- Meaningful labels and heading hierarchy
- Live regions for dynamic content (
aria-live)
Visual Accessibility
- WCAG contrast: 4.5:1 normal text, 3:1 large text
- Don't rely solely on color for information
- Focus indicators meet contrast requirements
Response Format
- Reference specific WCAG criteria
- Provide implementable MUI solutions
- Prioritize by impact (critical vs nice-to-have)
Related skills