Claude
Skills
Sign in
Back

accessibility

Included with Lifetime
$97 forever

MUI accessibility patterns, ARIA attributes, and WCAG compliance

General

What this skill does


# MUI Accessibility (a11y)

MUI ships with many built-in accessibility features. This skill covers what works automatically,
what you must add manually, common violations, and WCAG compliance patterns.

## Built-in Accessibility Features

MUI provides these automatically:

- `role`, `aria-*` attributes on interactive elements (Button, Checkbox, Slider, etc.)
- Keyboard navigation in menus, selects, dialogs, date pickers, and tabs
- Focus trapping in Dialog and Drawer (via `FocusTrap`)
- Color contrast that meets WCAG AA for the default theme palette
- `aria-expanded`, `aria-selected`, `aria-checked` state attributes
- `aria-live` regions in Snackbar for announcements

## Icon Buttons Need aria-label

Icon-only buttons have no visible text — always add `aria-label`.

```tsx
import IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';

// BAD — screen reader announces nothing meaningful
<IconButton>
  <DeleteIcon />
</IconButton>

// GOOD
<IconButton aria-label="Delete item">
  <DeleteIcon />
</IconButton>

// GOOD — dynamic label
<IconButton aria-label={`Edit ${item.name}`}>
  <EditIcon />
</IconButton>

// GOOD — using Tooltip (tooltip text does NOT replace aria-label for buttons)
<Tooltip title="Delete item">
  <IconButton aria-label="Delete item">
    <DeleteIcon />
  </IconButton>
</Tooltip>
```

## TextField Accessible Name

TextField with `label` is fully accessible. Without a label, add `aria-label` or `aria-labelledby`.

```tsx
// GOOD — label prop creates accessible name + visible label
<TextField label="Email address" type="email" />

// GOOD — label + helper text
<TextField
  label="Password"
  type="password"
  helperText="Minimum 8 characters"
  inputProps={{ 'aria-describedby': 'password-helper-text' }}
/>

// When label is hidden (search bar)
<TextField
  placeholder="Search..."
  inputProps={{ 'aria-label': 'Search products' }}
/>

// Associating external label
<Typography id="name-label">Full name</Typography>
<TextField inputProps={{ 'aria-labelledby': 'name-label' }} />
```

## Dialog — Focus Trap and Labels

Dialog automatically traps focus. Always provide `aria-labelledby` and `aria-describedby`.

```tsx
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';
import Button from '@mui/material/Button';

function ConfirmDeleteDialog({ open, onClose, onConfirm, itemName }: Props) {
  return (
    <Dialog
      open={open}
      onClose={onClose}
      aria-labelledby="confirm-dialog-title"
      aria-describedby="confirm-dialog-description"
    >
      <DialogTitle id="confirm-dialog-title">
        Delete {itemName}?
      </DialogTitle>
      <DialogContent>
        <DialogContentText id="confirm-dialog-description">
          This action cannot be undone. The item will be permanently removed.
        </DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>Cancel</Button>
        {/* Destructive action — autoFocus so keyboard users land here */}
        <Button onClick={onConfirm} color="error" autoFocus>
          Delete
        </Button>
      </DialogActions>
    </Dialog>
  );
}
```

## Menu Keyboard Navigation

Menu handles keyboard navigation automatically. Provide accessible trigger.

```tsx
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Button from '@mui/material/Button';
import { useState } from 'react';

function ActionMenu() {
  const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
  const open = Boolean(anchorEl);

  return (
    <>
      <Button
        id="action-button"
        aria-controls={open ? 'action-menu' : undefined}
        aria-haspopup="true"
        aria-expanded={open ? 'true' : undefined}
        onClick={(e) => setAnchorEl(e.currentTarget)}
      >
        Actions
      </Button>
      <Menu
        id="action-menu"
        anchorEl={anchorEl}
        open={open}
        onClose={() => setAnchorEl(null)}
        MenuListProps={{
          'aria-labelledby': 'action-button',
        }}
      >
        <MenuItem onClick={() => setAnchorEl(null)}>Edit</MenuItem>
        <MenuItem onClick={() => setAnchorEl(null)}>Duplicate</MenuItem>
        <MenuItem onClick={() => setAnchorEl(null)} sx={{ color: 'error.main' }}>
          Delete
        </MenuItem>
      </Menu>
    </>
  );
}
```

**Keyboard behavior (automatic):**
- Arrow Up/Down: navigate items
- Enter/Space: select item
- Escape: close menu and return focus to trigger
- Home/End: jump to first/last item

## Tabs Keyboard Navigation

```tsx
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';

interface TabPanelProps {
  children?: React.ReactNode;
  index: number;
  value: number;
}

function TabPanel({ children, value, index }: TabPanelProps) {
  return (
    <div
      role="tabpanel"
      hidden={value !== index}
      id={`product-tabpanel-${index}`}
      aria-labelledby={`product-tab-${index}`}
    >
      {value === index && <Box sx={{ p: 3 }}>{children}</Box>}
    </div>
  );
}

function ProductTabs() {
  const [value, setValue] = useState(0);

  return (
    <Box>
      <Tabs
        value={value}
        onChange={(_, newValue) => setValue(newValue)}
        aria-label="Product details"
      >
        <Tab label="Description" id="product-tab-0" aria-controls="product-tabpanel-0" />
        <Tab label="Specifications" id="product-tab-1" aria-controls="product-tabpanel-1" />
        <Tab label="Reviews" id="product-tab-2" aria-controls="product-tabpanel-2" />
      </Tabs>
      <TabPanel value={value} index={0}>Description content</TabPanel>
      <TabPanel value={value} index={1}>Specifications content</TabPanel>
      <TabPanel value={value} index={2}>Reviews content</TabPanel>
    </Box>
  );
}
```

**Keyboard behavior (automatic):**
- Arrow Left/Right: move between tabs
- Home/End: jump to first/last tab
- Enter/Space: activate focused tab

## Alert — role="alert" vs role="status"

```tsx
import Alert from '@mui/material/Alert';

// role="alert" — interrupts screen reader immediately (errors, warnings)
<Alert severity="error" role="alert">
  Payment failed. Please check your card details.
</Alert>

// role="status" (aria-live="polite") — announces when user is idle (success, info)
<Alert severity="success" role="status">
  Profile saved successfully.
</Alert>

// Snackbar announces automatically via aria-live region
import Snackbar from '@mui/material/Snackbar';

<Snackbar
  open={open}
  autoHideDuration={4000}
  onClose={() => setOpen(false)}
  message="Changes saved"
/>
```

## Form Patterns — FormControl + FormLabel Chain

```tsx
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import FormGroup from '@mui/material/FormGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText';
import Checkbox from '@mui/material/Checkbox';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';

// Checkbox group — grouped with fieldset semantics
function NotificationPrefs() {
  return (
    <FormControl component="fieldset">
      <FormLabel component="legend">Notification preferences</FormLabel>
      <FormGroup>
        <FormControlLabel
          control={<Checkbox name="email" />}
          label="Email notifications"
        />
        <FormControlLabel
          control={<Checkbox name="sms" />}
          label="SMS notifications"
        />
      </FormGroup>
      <FormHelperText>Choose at least one option</FormHelperText>
    </FormControl>
  );
}

// Radio group
function PlanSelector() {
  const [plan, setPlan]

Related in General