Claude
Skills
Sign in
Back

advanced-components

Included with Lifetime
$97 forever

Advanced MUI component patterns — Autocomplete (async, virtualized, grouped, createFilterOptions), Stepper (non-linear, vertical, mobile), Popover, Popper, Portal, ClickAwayListener, and complex composition

Design

What this skill does


# Advanced MUI Component Patterns

Deep patterns for complex MUI components that go beyond basic usage.

---

## Autocomplete — Advanced Patterns

### Async / Server-Side Options

```tsx
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import CircularProgress from '@mui/material/CircularProgress';
import { useState, useEffect, useRef } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

function AsyncAutocomplete() {
  const [open, setOpen] = useState(false);
  const [options, setOptions] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);
  const [inputValue, setInputValue] = useState('');
  const debounceRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    if (!open) return;
    if (!inputValue) {
      setOptions([]);
      return;
    }

    // Debounce API calls
    clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(async () => {
      setLoading(true);
      try {
        const res = await fetch(`/api/users?search=${encodeURIComponent(inputValue)}`);
        const data: User[] = await res.json();
        setOptions(data);
      } finally {
        setLoading(false);
      }
    }, 300);

    return () => clearTimeout(debounceRef.current);
  }, [inputValue, open]);

  return (
    <Autocomplete
      open={open}
      onOpen={() => setOpen(true)}
      onClose={() => setOpen(false)}
      options={options}
      loading={loading}
      getOptionLabel={(option) => option.name}
      isOptionEqualToValue={(option, value) => option.id === value.id}
      onInputChange={(_, value) => setInputValue(value)}
      filterOptions={(x) => x} // disable client-side filtering — server handles it
      renderInput={(params) => (
        <TextField
          {...params}
          label="Search users"
          slotProps={{
            input: {
              ...params.InputProps,
              endAdornment: (
                <>
                  {loading && <CircularProgress color="inherit" size={20} />}
                  {params.InputProps.endAdornment}
                </>
              ),
            },
          }}
        />
      )}
      renderOption={(props, option) => (
        <li {...props} key={option.id}>
          <Box>
            <Typography variant="body1">{option.name}</Typography>
            <Typography variant="caption" color="text.secondary">{option.email}</Typography>
          </Box>
        </li>
      )}
    />
  );
}
```

### createFilterOptions — Custom Filtering

```tsx
import { createFilterOptions } from '@mui/material/Autocomplete';

interface Film {
  title: string;
  year: number;
  inputValue?: string; // for "create" option
}

// Custom filter: match start of title, limit to 10
const filter = createFilterOptions<Film>({
  matchFrom: 'start',    // 'start' | 'any' (default: 'any')
  limit: 10,             // max options shown
  stringify: (option) => option.title, // what to search against
  ignoreAccents: true,   // normalize accents
  ignoreCase: true,      // case-insensitive (default: true)
  trim: true,            // trim whitespace
});

// "Create" option pattern — add user-typed value as new option
<Autocomplete
  freeSolo
  selectOnFocus
  clearOnBlur
  handleHomeEndKeys
  options={films}
  filterOptions={(options, params) => {
    const filtered = filter(options, params);

    const { inputValue } = params;
    // Suggest creating a new value
    const isExisting = options.some((option) => inputValue === option.title);
    if (inputValue !== '' && !isExisting) {
      filtered.push({
        inputValue,
        title: `Add "${inputValue}"`,
        year: new Date().getFullYear(),
      });
    }
    return filtered;
  }}
  getOptionLabel={(option) => {
    if (typeof option === 'string') return option;
    if (option.inputValue) return option.inputValue;
    return option.title;
  }}
  onChange={(_, newValue) => {
    if (newValue && typeof newValue !== 'string' && newValue.inputValue) {
      // User chose "Add ..." — create the new item
      handleCreate(newValue.inputValue);
    }
  }}
  renderInput={(params) => <TextField {...params} label="Film" />}
/>
```

### Virtualized Autocomplete (10,000+ options)

```tsx
import { FixedSizeList, ListChildComponentProps } from 'react-window';
import { forwardRef, HTMLAttributes } from 'react';

const ITEM_HEIGHT = 36;
const LISTBOX_PADDING = 8;

function renderRow(props: ListChildComponentProps) {
  const { data, index, style } = props;
  const dataSet = data[index];
  const inlineStyle = {
    ...style,
    top: (style.top as number) + LISTBOX_PADDING,
  };
  return (
    <li {...dataSet[0]} style={inlineStyle} key={dataSet[1].id}>
      {dataSet[1].label}
    </li>
  );
}

const ListboxComponent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLElement>>(
  function ListboxComponent(props, ref) {
    const { children, ...other } = props;
    const items = children as [HTMLAttributes<HTMLElement>, { label: string; id: string }][];
    const itemCount = items.length;
    const height = Math.min(8, itemCount) * ITEM_HEIGHT + 2 * LISTBOX_PADDING;

    return (
      <div ref={ref} {...other}>
        <FixedSizeList
          height={height}
          width="100%"
          itemSize={ITEM_HEIGHT}
          itemCount={itemCount}
          itemData={items}
          overscanCount={5}
        >
          {renderRow}
        </FixedSizeList>
      </div>
    );
  }
);

<Autocomplete
  disableListWrap
  slots={{ listbox: ListboxComponent }}
  options={tenThousandOptions}
  renderInput={(params) => <TextField {...params} label="10,000 options" />}
/>
```

### Grouped Options

```tsx
<Autocomplete
  options={countries.sort((a, b) => -b.continent.localeCompare(a.continent))}
  groupBy={(option) => option.continent}
  getOptionLabel={(option) => option.name}
  renderGroup={(params) => (
    <li key={params.key}>
      <GroupHeader>{params.group}</GroupHeader>
      <GroupItems>{params.children}</GroupItems>
    </li>
  )}
  renderInput={(params) => <TextField {...params} label="Country" />}
/>
```

### Multiple with Chip Limit

```tsx
<Autocomplete
  multiple
  limitTags={2}           // show max 2 chips, "+N" for overflow
  disableCloseOnSelect    // keep dropdown open after selection
  options={allTags}
  getOptionLabel={(option) => option.label}
  renderTags={(value, getTagProps, ownerState) =>
    value.map((option, index) => {
      const { key, ...tagProps } = getTagProps({ index });
      return (
        <Chip
          key={key}
          label={option.label}
          size="small"
          color="primary"
          variant="outlined"
          {...tagProps}
        />
      );
    })
  }
  renderInput={(params) => <TextField {...params} label="Tags" placeholder="Add..." />}
/>
```

### Highlight Matching Text

```tsx
import parse from 'autosuggest-highlight/parse';
import match from 'autosuggest-highlight/match';

<Autocomplete
  options={options}
  renderOption={(props, option, { inputValue }) => {
    const matches = match(option.label, inputValue, { insideWords: true });
    const parts = parse(option.label, matches);
    return (
      <li {...props}>
        {parts.map((part, index) => (
          <span key={index} style={{ fontWeight: part.highlight ? 700 : 400 }}>
            {part.text}
          </span>
        ))}
      </li>
    );
  }}
  renderInput={(params) => <TextField {...params} label="Highlight demo" />}
/>
```

---

## Stepper — Advanced Patterns

### Horizontal Stepper with Validation

```tsx
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import StepContent from '@mui/material/StepContent';
import Button from '@mui/material/Button';

const steps = ['Account', 'Profile', 'Confirm'];

function HorizontalStepper() {
  const [activeStep, setActiveStep] = useState(0);
  const [errors, setErrors] = useState<Record<number, string>>({});

  const va

Related in Design