Agent Skills: MUI Core Components Reference

MUI core component patterns and best practices

UncategorizedID: lobbi-docs/claude/components

Install this agent skill to your local

pnpm dlx add-skill https://github.com/markus41/claude/tree/HEAD/plugins/mui-expert/skills/components

Skill Files

Browse the full folder contents for components.

Download Skill

Loading file tree…

plugins/mui-expert/skills/components/SKILL.md

Skill Metadata

Name
components
Description
MUI core component patterns and best practices

MUI Core Components Reference

Input Components

TextField

The most common form input. Wraps FormControl, InputLabel, OutlinedInput/FilledInput/Input, and FormHelperText in one component.

import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';

// Standard usage
<TextField
  label="Email address"
  type="email"
  variant="outlined"          // 'outlined' | 'filled' | 'standard'
  size="small"                // 'small' | 'medium'
  fullWidth
  required
  value={email}
  onChange={(e) => setEmail(e.target.value)}
  error={!!emailError}
  helperText={emailError || 'We will never share your email'}
  InputProps={{
    startAdornment: <InputAdornment position="start"><EmailIcon /></InputAdornment>,
  }}
  inputProps={{ maxLength: 100, 'aria-label': 'email address' }}
/>

// Multiline / textarea
<TextField
  label="Description"
  multiline
  rows={4}
  // or: minRows={2} maxRows={8} for auto-grow
  fullWidth
/>

Autocomplete

Combines a text input with a dropdown for both free-form and constrained selection.

import Autocomplete from '@mui/material/Autocomplete';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';

// Static options
<Autocomplete
  options={countries}
  getOptionLabel={(option) => option.label}
  isOptionEqualToValue={(option, value) => option.code === value.code}
  value={selectedCountry}
  onChange={(_, newValue) => setSelectedCountry(newValue)}
  renderInput={(params) => (
    <TextField {...params} label="Country" />
  )}
/>

// Multiple selection with chips
<Autocomplete
  multiple
  options={tags}
  value={selectedTags}
  onChange={(_, newValue) => setSelectedTags(newValue)}
  renderTags={(value, getTagProps) =>
    value.map((option, index) => (
      <Chip label={option} {...getTagProps({ index })} key={option} />
    ))
  }
  renderInput={(params) => (
    <TextField {...params} label="Tags" placeholder="Add tag" />
  )}
/>

// Async / server-side options
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState([]);
const [loading, setLoading] = React.useState(false);

<Autocomplete
  open={open}
  onOpen={() => { setOpen(true); fetchOptions(); }}
  onClose={() => setOpen(false)}
  options={options}
  loading={loading}
  renderInput={(params) => (
    <TextField
      {...params}
      label="Search users"
      InputProps={{
        ...params.InputProps,
        endAdornment: (
          <>
            {loading && <CircularProgress size={20} />}
            {params.InputProps.endAdornment}
          </>
        ),
      }}
    />
  )}
/>

Select

import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Checkbox from '@mui/material/Checkbox';
import ListItemText from '@mui/material/ListItemText';

<FormControl fullWidth size="small">
  <InputLabel id="role-label">Role</InputLabel>
  <Select
    labelId="role-label"
    label="Role"
    value={role}
    onChange={(e) => setRole(e.target.value)}
  >
    <MenuItem value="admin">Administrator</MenuItem>
    <MenuItem value="editor">Editor</MenuItem>
    <MenuItem value="viewer">Viewer</MenuItem>
  </Select>
</FormControl>

// Multiple select with checkboxes
<Select
  multiple
  value={selectedRoles}
  onChange={handleChange}
  renderValue={(selected) => (
    <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
      {selected.map((value) => <Chip key={value} label={value} size="small" />)}
    </Box>
  )}
>
  {roles.map((role) => (
    <MenuItem key={role} value={role}>
      <Checkbox checked={selectedRoles.includes(role)} />
      <ListItemText primary={role} />
    </MenuItem>
  ))}
</Select>

Checkbox, Radio, Switch

import Checkbox from '@mui/material/Checkbox';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import Switch from '@mui/material/Switch';
import FormControlLabel from '@mui/material/FormControlLabel';

// Checkbox with indeterminate state
<Checkbox
  checked={allSelected}
  indeterminate={someSelected && !allSelected}
  onChange={handleSelectAll}
/>

// Radio group
<RadioGroup value={alignment} onChange={(e) => setAlignment(e.target.value)}>
  <FormControlLabel value="left" control={<Radio />} label="Left" />
  <FormControlLabel value="center" control={<Radio />} label="Center" />
  <FormControlLabel value="right" control={<Radio />} label="Right" />
</RadioGroup>

// Switch
<FormControlLabel
  control={
    <Switch checked={darkMode} onChange={(e) => setDarkMode(e.target.checked)} />
  }
  label="Dark mode"
/>

Slider and Rating

import Slider from '@mui/material/Slider';
import Rating from '@mui/material/Rating';

// Range slider
<Slider
  value={priceRange}
  onChange={(_, newValue) => setPriceRange(newValue as number[])}
  valueLabelDisplay="auto"
  min={0}
  max={1000}
  step={10}
  marks={[
    { value: 0, label: '$0' },
    { value: 500, label: '$500' },
    { value: 1000, label: '$1000' },
  ]}
/>

// Star rating
<Rating
  value={rating}
  onChange={(_, newValue) => setRating(newValue)}
  precision={0.5}
  size="large"
/>

Display Components

Typography

import Typography from '@mui/material/Typography';

// Semantic element with visual variant
<Typography variant="h1" component="h2">Page title</Typography>

// Caption with ellipsis
<Typography
  variant="body2"
  color="text.secondary"
  noWrap
  sx={{ maxWidth: 200 }}
>
  Long text that will be truncated
</Typography>

// Paragraph with bottom margin
<Typography variant="body1" gutterBottom>
  First paragraph with bottom margin.
</Typography>

Chip

import Chip from '@mui/material/Chip';
import Avatar from '@mui/material/Avatar';

<Chip label="Active" color="success" size="small" />
<Chip label="Draft" variant="outlined" onDelete={handleDelete} />
<Chip
  avatar={<Avatar alt="User" src="/user.jpg" />}
  label="Jane Smith"
  onClick={handleClick}
  clickable
/>

Avatar and Badge

import Avatar from '@mui/material/Avatar';
import AvatarGroup from '@mui/material/AvatarGroup';
import Badge from '@mui/material/Badge';

// Avatar with fallback initials
<Avatar src="/user.jpg" alt="John Doe">JD</Avatar>

// Avatar group with overflow count
<AvatarGroup max={4}>
  {users.map((u) => <Avatar key={u.id} src={u.avatar} alt={u.name} />)}
</AvatarGroup>

// Notification badge on icon
<Badge badgeContent={unreadCount} color="error" max={99}>
  <NotificationsIcon />
</Badge>

// Online indicator dot
<Badge
  overlap="circular"
  anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
  variant="dot"
  color="success"
>
  <Avatar src="/user.jpg" />
</Badge>

Tooltip

import Tooltip from '@mui/material/Tooltip';

<Tooltip title="Delete this item" placement="top" arrow>
  <IconButton aria-label="delete"><DeleteIcon /></IconButton>
</Tooltip>

// Tooltip on disabled element (needs a wrapping span)
<Tooltip title="You don't have permission">
  <span>
    <Button disabled>Restricted action</Button>
  </span>
</Tooltip>

Alert

import Alert from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';

<Alert severity="warning" variant="filled" onClose={handleClose}>
  <AlertTitle>Warning</AlertTitle>
  Your subscription expires in 3 days.
</Alert>

// severity: 'error' | 'warning' | 'info' | 'success'
// variant: 'standard' | 'filled' | 'outlined'

Table

import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';

<TableContainer component={Paper} sx={{ maxHeight: 440 }}>
  <Table stickyHeader aria-label="users table" size="small">
    <TableHead>
      <TableRow>
        <TableCell>Name</TableCell>
        <TableCell align="right">Age</TableCell>
        <TableCell>Status</TableCell>
      </TableRow>
    </TableHead>
    <TableBody>
      {rows.map((row) => (
        <TableRow
          key={row.id}
          hover
          selected={selectedId === row.id}
          onClick={() => setSelectedId(row.id)}
          sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
        >
          <TableCell component="th" scope="row">{row.name}</TableCell>
          <TableCell align="right">{row.age}</TableCell>
          <TableCell>
            <Chip
              label={row.status}
              color={row.status === 'active' ? 'success' : 'default'}
              size="small"
            />
          </TableCell>
        </TableRow>
      ))}
    </TableBody>
  </Table>
</TableContainer>

Navigation Components

AppBar

import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';

<AppBar position="sticky" color="default" elevation={1}>
  <Toolbar>
    <IconButton edge="start" onClick={toggleDrawer} aria-label="open menu">
      <MenuIcon />
    </IconButton>
    <Typography variant="h6" sx={{ flexGrow: 1 }}>App Name</Typography>
    <Button color="inherit" startIcon={<LoginIcon />}>Sign in</Button>
  </Toolbar>
</AppBar>

Drawer

import Drawer from '@mui/material/Drawer';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';

// Temporary drawer (modal overlay)
<Drawer
  anchor="left"
  open={drawerOpen}
  onClose={() => setDrawerOpen(false)}
  PaperProps={{ sx: { width: 280 } }}
>
  <Box role="presentation" onClick={() => setDrawerOpen(false)}>
    <List>
      {navItems.map((item) => (
        <ListItemButton
          key={item.path}
          selected={pathname === item.path}
          component={RouterLink}
          to={item.path}
        >
          <ListItemIcon>{item.icon}</ListItemIcon>
          <ListItemText primary={item.label} />
        </ListItemButton>
      ))}
    </List>
  </Box>
</Drawer>

// Permanent drawer for desktop
<Drawer
  variant="permanent"
  sx={{ width: 240, '& .MuiDrawer-paper': { width: 240, boxSizing: 'border-box' } }}
>
  {drawerContent}
</Drawer>

Tabs

import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';

<Tabs
  value={activeTab}
  onChange={(_, newValue) => setActiveTab(newValue)}
  aria-label="main navigation tabs"
  indicatorColor="primary"
  textColor="primary"
  variant="scrollable"
  scrollButtons="auto"
>
  <Tab label="Overview" value="overview" />
  <Tab label="Analytics" value="analytics" />
  <Tab label="Settings" value="settings" disabled />
</Tabs>

Menu

import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Divider from '@mui/material/Divider';

const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);

<IconButton onClick={(e) => setAnchorEl(e.currentTarget)} aria-label="account menu">
  <AccountCircleIcon />
</IconButton>
<Menu
  anchorEl={anchorEl}
  open={Boolean(anchorEl)}
  onClose={() => setAnchorEl(null)}
  transformOrigin={{ horizontal: 'right', vertical: 'top' }}
  anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
  <MenuItem onClick={handleProfile}>
    <ListItemIcon><PersonIcon /></ListItemIcon>
    Profile
  </MenuItem>
  <Divider />
  <MenuItem onClick={handleLogout} sx={{ color: 'error.main' }}>
    <ListItemIcon><LogoutIcon color="error" /></ListItemIcon>
    Logout
  </MenuItem>
</Menu>

Breadcrumbs and Pagination

import Breadcrumbs from '@mui/material/Breadcrumbs';
import Pagination from '@mui/material/Pagination';

// Breadcrumbs
<Breadcrumbs aria-label="breadcrumb">
  <Link component={RouterLink} to="/" underline="hover" color="inherit">Home</Link>
  <Link component={RouterLink} to="/products" underline="hover" color="inherit">Products</Link>
  <Typography color="text.primary">Laptop</Typography>
</Breadcrumbs>

// Pagination
<Pagination
  count={totalPages}
  page={currentPage}
  onChange={(_, page) => setCurrentPage(page)}
  color="primary"
  size="small"
  siblingCount={1}
  boundaryCount={1}
  showFirstButton
  showLastButton
/>

Feedback Components

Dialog

import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';

<Dialog
  open={open}
  onClose={handleClose}
  maxWidth="sm"
  fullWidth
  aria-labelledby="confirm-dialog-title"
>
  <DialogTitle id="confirm-dialog-title">Confirm deletion</DialogTitle>
  <DialogContent>
    <DialogContentText>
      Are you sure you want to delete <strong>{itemName}</strong>? This action cannot be undone.
    </DialogContentText>
  </DialogContent>
  <DialogActions>
    <Button onClick={handleClose}>Cancel</Button>
    <Button onClick={handleConfirm} color="error" variant="contained" autoFocus>
      Delete
    </Button>
  </DialogActions>
</Dialog>

Snackbar

import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';

// Simple message
<Snackbar
  open={snackbarOpen}
  autoHideDuration={4000}
  onClose={() => setSnackbarOpen(false)}
  message="Changes saved"
  anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
/>

// With Alert for severity styling
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
  <Alert onClose={handleClose} severity="success" variant="filled" sx={{ width: '100%' }}>
    Profile updated successfully.
  </Alert>
</Snackbar>

Progress and Loading States

import CircularProgress from '@mui/material/CircularProgress';
import LinearProgress from '@mui/material/LinearProgress';
import Backdrop from '@mui/material/Backdrop';
import Skeleton from '@mui/material/Skeleton';

// Indeterminate spinner
<CircularProgress size={24} />

// Determinate with percentage
<CircularProgress variant="determinate" value={uploadPercent} />

// Linear progress bar
<LinearProgress
  variant="determinate"
  value={progress}
  sx={{ height: 8, borderRadius: 4 }}
/>

// Loading overlay
{loading && (
  <Backdrop open sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, color: '#fff' }}>
    <CircularProgress color="inherit" />
  </Backdrop>
)}

// Skeleton placeholder cards
{loading
  ? Array.from({ length: 6 }).map((_, i) => (
      <Card key={i}>
        <Skeleton variant="rectangular" height={140} />
        <CardContent>
          <Skeleton variant="text" width="80%" />
          <Skeleton variant="text" width="60%" />
        </CardContent>
      </Card>
    ))
  : items.map((item) => <ItemCard key={item.id} item={item} />)
}

Layout: Card and Paper

import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import CardActions from '@mui/material/CardActions';
import CardHeader from '@mui/material/CardHeader';
import Paper from '@mui/material/Paper';

<Card sx={{ maxWidth: 345 }} elevation={2}>
  <CardHeader
    avatar={<Avatar>{user.initials}</Avatar>}
    title={user.name}
    subheader={formatDate(post.createdAt)}
    action={<IconButton aria-label="settings"><MoreVertIcon /></IconButton>}
  />
  <CardMedia component="img" height="140" image={post.thumbnail} alt={post.title} />
  <CardContent>
    <Typography variant="body2" color="text.secondary">{post.excerpt}</Typography>
  </CardContent>
  <CardActions>
    <Button size="small">Read more</Button>
    <IconButton aria-label="like"><FavoriteIcon /></IconButton>
    <IconButton aria-label="share"><ShareIcon /></IconButton>
  </CardActions>
</Card>

// Paper as a surface container
<Paper elevation={0} variant="outlined" sx={{ p: 3, borderRadius: 2 }}>
  {children}
</Paper>

Layout: List

import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';

<List
  subheader={<ListSubheader component="div">Recent files</ListSubheader>}
  sx={{ width: '100%', maxWidth: 360 }}
>
  {files.map((file) => (
    <React.Fragment key={file.id}>
      <ListItem
        disablePadding
        secondaryAction={
          <IconButton edge="end" onClick={() => handleDelete(file.id)}>
            <DeleteIcon />
          </IconButton>
        }
      >
        <ListItemButton onClick={() => handleOpen(file)}>
          <ListItemIcon><InsertDriveFileIcon /></ListItemIcon>
          <ListItemText primary={file.name} secondary={formatBytes(file.size)} />
        </ListItemButton>
      </ListItem>
      <Divider component="li" />
    </React.Fragment>
  ))}
</List>

Utility Components

ClickAwayListener

Detects clicks outside the wrapped element — essential for custom dropdowns and popovers.

import ClickAwayListener from '@mui/material/ClickAwayListener';

<ClickAwayListener onClickAway={handleClose}>
  <Box sx={{ position: 'relative' }}>
    <Button onClick={toggleOpen}>Menu</Button>
    {open && (
      <Paper sx={{ position: 'absolute', top: '100%', zIndex: 1, width: 200 }}>
        <MenuItem onClick={handleClose}>Option 1</MenuItem>
        <MenuItem onClick={handleClose}>Option 2</MenuItem>
      </Paper>
    )}
  </Box>
</ClickAwayListener>

Portal

Renders children into document.body or a custom container.

import Portal from '@mui/material/Portal';

<Portal>
  <Box sx={{ position: 'fixed', bottom: 16, right: 16, zIndex: 'tooltip' }}>
    <Fab color="primary"><AddIcon /></Fab>
  </Box>
</Portal>

NoSsr — Client-Only Rendering

import NoSsr from '@mui/material/NoSsr';

<NoSsr fallback={<Skeleton variant="rectangular" height={300} />}>
  <MapComponent /> {/* Depends on window — fails on server */}
</NoSsr>

useMediaQuery — Responsive Conditional Rendering

import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';

function ResponsiveNav() {
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down('md'));

  return isMobile ? <MobileBottomNav /> : <DesktopSidebar />;
}

// Without theme — raw media query string
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');

// With SSR safety — avoid hydration mismatch
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'), {
  noSsr: true,                    // skip server render for this query
  defaultMatches: true,           // assume true on server
  // or: ssrMatchMedia for custom server-side matching
});

Component Prop (Polymorphic Rendering)

Most MUI components accept component to change the rendered HTML element or use a router link:

import { Link as RouterLink } from 'react-router-dom';

// Button as router link
<Button component={RouterLink} to="/dashboard" variant="contained">
  Dashboard
</Button>

// ListItemButton as router link
<ListItemButton component={RouterLink} to="/settings">
  <ListItemIcon><SettingsIcon /></ListItemIcon>
  <ListItemText primary="Settings" />
</ListItemButton>

// Typography as a label
<Typography component="label" htmlFor="email-input" variant="body2">
  Email Address
</Typography>

// Card as semantic article
<Card component="article">
  <CardContent>...</CardContent>
</Card>

// Box as section with semantic HTML
<Box component="section" aria-labelledby="section-title">
  <Typography id="section-title" variant="h2">Title</Typography>
</Box>

Global Configuration via Theme defaultProps

Set defaults for every instance of a component across the app:

const theme = createTheme({
  components: {
    MuiButton: {
      defaultProps: {
        variant: 'contained',
        disableElevation: true,
        size: 'medium',
      },
    },
    MuiTextField: {
      defaultProps: {
        variant: 'outlined',
        size: 'small',
        fullWidth: true,
      },
    },
    MuiTooltip: {
      defaultProps: {
        arrow: true,
        enterDelay: 500,
      },
    },
    MuiAlert: {
      defaultProps: {
        variant: 'filled',
      },
    },
    MuiChip: {
      defaultProps: {
        size: 'small',
      },
    },
  },
});

Now <Button>Save</Button> renders as contained by default — no need to repeat variant="contained" everywhere.