---
productId: material-ui
title: React Autocomplete component
components: TextField, Popper, Autocomplete
githubLabel: 'scope: autocomplete'
waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
githubSource: packages/mui-material/src/Autocomplete
---

# Autocomplete

A text input that suggests matching options as you type.

Autocomplete supports three core interaction modes:

1. Pick a single value from a predefined list, like a country picker: [Combobox](#combobox).
2. Pick multiple values shown as chips, like a tag picker: [Multiple values](#multiple-values).
3. Accept any text with suggestions, like a search field: [Free solo](#free-solo).



## Usage guidelines

- **Use for filterable choices**: Autocomplete is best for lists that are too long to scan. Use [Select](/material-ui/react-select/) instead for short lists.
- **Form controls must have an accessible name**: Use a visible `TextField` `label` when possible, or add `aria-label` if the label is hidden.
- **Keep the popup option-only**: The listbox should only contain selectable options. Avoid buttons, links, or non-option controls such as "Select all" because they disrupt keyboard semantics and assistive technology behavior.

## Combobox

Use the basic combobox when users need to search a predefined list and pick one value.

```tsx
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import top100Films from './top100Films';

export default function ComboBox() {
  return (
    <Autocomplete
      disablePortal
      options={top100Films}
      sx={{ width: 300 }}
      renderInput={(params) => <TextField {...params} label="Movie" />}
    />
  );
}

```

### Options structure

By default, options can be strings or objects with a `label` string. You can add fields that model your data, such as a stable ID, timestamp, or grouping field. TypeScript infers the option type from the `options` prop, so callbacks like `onChange` are strongly typed.

```ts
interface AutocompleteOption {
  label: string;
  id?: string | number;
}
// or
type AutocompleteOption = string;
```

For example:

```js
const options = [
  { label: 'The Godfather', id: 1 },
  { label: 'Pulp Fiction', id: 2 },
];
// or
const options = ['The Godfather', 'Pulp Fiction'];
```

When using object options, you must provide `isOptionEqualToValue` so the component can match the current value to the right option. The default comparison uses strict equality (`===`), which only works when the value reference is the same as one of the options:

```tsx
<Autocomplete
  options={options}
  isOptionEqualToValue={(option, value) => option.id === value.id}
/>
```

To display a value other than `label`, use `getOptionLabel` to return a string representing each option:

```tsx
const options = [
  { id: '1', email: 'alice@example.com' },
  { id: '2', email: 'bob@example.com' },
];

<Autocomplete options={options} getOptionLabel={(option) => option.email} />;
```

Autocomplete uses the label as the React key for each option by default. If two options share the same label, keep `getOptionLabel` for the display text and use `getOptionKey` to provide a stable key for each rendered option:

```tsx
// Two contacts happen to share the same display name
const options = [
  { label: 'John Smith', id: 'usr_4f12a7b8' },
  { label: 'John Smith', id: 'usr_e9c3d521' },
];

<Autocomplete
  options={options}
  getOptionLabel={(option) => option.label}
  getOptionKey={(option) => option.id}
/>;
```

### Playground

Each example below demonstrates one feature.

```tsx
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Stack from '@mui/material/Stack';
import top100Films from './top100Films';

type Film = (typeof top100Films)[number];

const filmTitles = top100Films.map((option) => option.label);
const defaultProps = {
  options: top100Films,
};
const flatProps = {
  options: filmTitles,
};

export default function Playground() {
  const [value, setValue] = React.useState<Film | null>(null);

  return (
    <Stack spacing={2} sx={{ width: 300 }}>
      <Autocomplete
        {...defaultProps}
        disableCloseOnSelect
        renderInput={(params) => (
          <TextField {...params} label="disableCloseOnSelect" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        clearOnEscape
        renderInput={(params) => (
          <TextField {...params} label="clearOnEscape" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        disableClearable
        renderInput={(params) => (
          <TextField {...params} label="disableClearable" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        includeInputInList
        renderInput={(params) => (
          <TextField {...params} label="includeInputInList" variant="filled" />
        )}
      />
      <Autocomplete
        {...flatProps}
        renderInput={(params) => (
          <TextField {...params} label="flat" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        value={value}
        onChange={(event, newValue) => {
          setValue(newValue);
        }}
        renderInput={(params) => (
          <TextField {...params} label="controlled" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        autoComplete
        includeInputInList
        renderInput={(params) => (
          <TextField {...params} label="autoComplete" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        disableListWrap
        renderInput={(params) => (
          <TextField {...params} label="disableListWrap" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        openOnFocus
        renderInput={(params) => (
          <TextField {...params} label="openOnFocus" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        autoHighlight
        renderInput={(params) => (
          <TextField {...params} label="autoHighlight" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        autoSelect
        renderInput={(params) => (
          <TextField {...params} label="autoSelect" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        disabled
        renderInput={(params) => (
          <TextField {...params} label="disabled" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        disablePortal
        renderInput={(params) => (
          <TextField {...params} label="disablePortal" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        blurOnSelect
        renderInput={(params) => (
          <TextField {...params} label="blurOnSelect" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        clearOnBlur
        renderInput={(params) => (
          <TextField {...params} label="clearOnBlur" variant="filled" />
        )}
      />
      <Autocomplete
        {...defaultProps}
        selectOnFocus
        renderInput={(params) => (
          <TextField {...params} label="selectOnFocus" variant="filled" />
        )}
      />
      <Autocomplete
        {...flatProps}
        readOnly
        defaultValue={flatProps.options[13]}
        renderInput={(params) => (
          <TextField {...params} label="readOnly" variant="filled" />
        )}
      />
    </Stack>
  );
}

```

### Country select

Use `renderOption` to customize each option. This country picker renders a flag, country code, and calling code.

```tsx
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import countries from './countries';

export default function CountrySelect() {
  return (
    <Autocomplete
      sx={{ width: 320 }}
      options={countries}
      autoHighlight
      getOptionLabel={(option) => option.label}
      renderOption={(props, option) => {
        const { key, ...optionProps } = props;
        return (
          <Box
            key={key}
            component="li"
            sx={{ '& > img': { mr: 2, flexShrink: 0 } }}
            {...optionProps}
          >
            <img
              loading="lazy"
              decoding="async"
              width={20}
              src={`https://flagcdn.com/${option.code.toLowerCase()}.svg`}
              alt=""
            />
            <Box
              component="span"
              sx={{
                minWidth: 0,
                overflow: 'hidden',
                textOverflow: 'ellipsis',
                whiteSpace: 'nowrap',
              }}
            >
              {option.label} ({option.code})
            </Box>
            <Box
              component="span"
              sx={{ ml: 'auto', flexShrink: 0, whiteSpace: 'nowrap' }}
            >
              +{option.countryCallingCode}
            </Box>
          </Box>
        );
      }}
      renderInput={(params) => (
        <TextField
          {...params}
          label="Choose a country"
          slotProps={{
            ...params.slotProps,
            htmlInput: {
              ...params.slotProps.htmlInput,
              autoComplete: 'new-password', // disable autocomplete and autofill
            },
          }}
        />
      )}
    />
  );
}

```

### Controlled states

Autocomplete has two states that can be controlled independently:

1. **`value`** with `value`/`onChange`—the option the user has selected, set when they press <kbd class="key">Enter</kbd> or click an option.
2. **`inputValue`** with `inputValue`/`onInputChange`—the text currently shown in the textbox.

Control them independently—the two states aren't linked.

:::info

- A component is **controlled** when its parent manages it through props.
- A component is **uncontrolled** when it manages its own local state.

Learn more about controlled and uncontrolled components in the [React documentation](https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components).
:::

```tsx
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';

interface ProgrammingLanguage {
  id: string;
  label: string;
}

const langs: ProgrammingLanguage[] = [
  { id: 'js', label: 'JavaScript' },
  { id: 'ts', label: 'TypeScript' },
  { id: 'py', label: 'Python' },
  { id: 'java', label: 'Java' },
  { id: 'cpp', label: 'C++' },
  { id: 'cs', label: 'C#' },
  { id: 'php', label: 'PHP' },
  { id: 'ruby', label: 'Ruby' },
  { id: 'go', label: 'Go' },
  { id: 'rust', label: 'Rust' },
  { id: 'swift', label: 'Swift' },
];

export default function ControllableStates() {
  const [value, setValue] = React.useState<ProgrammingLanguage | null>(langs[0]);
  const [inputValue, setInputValue] = React.useState('');

  return (
    <Stack spacing={3} sx={{ width: 300 }}>
      <Stack
        spacing={0.5}
        sx={{
          typography: 'body1',
          color: 'text.secondary',
        }}
      >
        <div>
          value:{' '}
          <Box component="code" sx={{ color: 'text.primary' }}>
            {value?.label ?? 'null'}
          </Box>
        </div>
        <div>
          inputValue:{' '}
          <Box component="code" sx={{ color: 'text.primary' }}>
            {`"${inputValue}"`}
          </Box>
        </div>
      </Stack>
      <Autocomplete
        value={value}
        onChange={(_event, newValue) => {
          setValue(newValue);
        }}
        inputValue={inputValue}
        onInputChange={(_event, newInputValue) => {
          setInputValue(newInputValue);
        }}
        options={langs}
        isOptionEqualToValue={(option, selectedValue) =>
          option.id === selectedValue.id
        }
        sx={{ width: 1 }}
        renderInput={(params) => <TextField {...params} label="Languages" />}
      />
    </Stack>
  );
}

```

:::warning

If you control `value`, keep the reference stable between renders by passing the same array or object if its contents haven't changed.

```tsx
// ⚠️ BAD
return <Autocomplete multiple value={allValues.filter((v) => v.selected)} />;

// 👍 GOOD
const selectedValues = React.useMemo(
  () => allValues.filter((v) => v.selected),
  [allValues],
);
return <Autocomplete multiple value={selectedValues} />;
```

In the first example, `allValues.filter` returns **a new array** every render. Wrapping it in `useMemo` ensures the array only changes when its contents change.
:::

### Disabled options

Mark specific options as disabled with the `getOptionDisabled` prop.

```tsx
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';

// One time slot every 30 minutes.
const timeSlots = Array.from(
  { length: 24 * 2 },
  (_, index) =>
    `${index < 20 ? '0' : ''}${Math.floor(index / 2)}:${
      index % 2 === 0 ? '00' : '30'
    }`,
);

const unavailableTimeSlots = new Set(
  timeSlots.filter((_, index) => index % 4 === 0),
);

export default function DisabledOptions() {
  return (
    <Autocomplete
      options={timeSlots}
      getOptionDisabled={(option) => unavailableTimeSlots.has(option)}
      sx={{ width: 300 }}
      renderInput={(params) => <TextField {...params} label="Appointment time" />}
    />
  );
}

```

### Grouped

Group options with the `groupBy` prop. Sort the options by the same field you're grouping on—otherwise the same group header repeats.

```tsx
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import top100Films from './top100Films';

const options = top100Films
  .map((option) => {
    const firstLetter = option.label[0].toUpperCase();
    return {
      firstLetter: /[0-9]/.test(firstLetter) ? '0-9' : firstLetter,
      ...option,
    };
  })
  .sort((a, b) => a.firstLetter.localeCompare(b.firstLetter));

export default function Grouped() {
  return (
    <Autocomplete
      options={options}
      groupBy={(option) => option.firstLetter}
      sx={{ width: 300 }}
      renderInput={(params) => <TextField {...params} label="With categories" />}
    />
  );
}

```

### Custom group rendering

Customize how groups render with the `renderGroup` prop. It receives an object with:

- `key`—the React key to apply to the rendered group
- `group`—the group name string
- `children`—the list items in that group

The demo below groups countries by continent and customizes the group rendering.

```tsx
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import { styled } from '@mui/material/styles';
import countries from './countries';

const options = [...countries].sort(
  (a, b) => a.continent.localeCompare(b.continent) || a.label.localeCompare(b.label),
);

const Group = styled('li')(({ theme }) => ({
  '& + &': {
    marginTop: theme.spacing(0.5),
    paddingTop: theme.spacing(0.5),
    borderTop: `1px solid ${(theme.vars || theme).palette.divider}`,
  },
}));

const GroupHeader = styled('div')(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1.25, 2, 0.75),
  color: (theme.vars || theme).palette.text.secondary,
  fontWeight: theme.typography.fontWeightMedium,
}));

const GroupItems = styled('ul')({
  padding: 0,
});

export default function RenderGroup() {
  return (
    <Autocomplete
      options={options}
      groupBy={(option) => option.continent}
      sx={{ width: 300 }}
      renderInput={(params) => <TextField {...params} label="Choose a country" />}
      renderGroup={(params) => (
        <Group key={params.key}>
          <GroupHeader>{params.group}</GroupHeader>
          <GroupItems>{params.children}</GroupItems>
        </Group>
      )}
    />
  );
}

```

## Free solo

Use `freeSolo` when the input should accept values outside the provided options.

### Search input

Designed for **search inputs** with suggestions—for example, Google search or a typeahead field.

```tsx
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import Autocomplete from '@mui/material/Autocomplete';
import top100Films from './top100Films';

const filmTitles = top100Films.map((option) => option.label);

export default function FreeSolo() {
  return (
    <Stack spacing={2} sx={{ width: 300 }}>
      <Autocomplete
        freeSolo
        resetHighlightOnMouseLeave
        options={filmTitles}
        renderInput={(params) => <TextField {...params} label="freeSolo" />}
      />
      <Autocomplete
        freeSolo
        resetHighlightOnMouseLeave
        disableClearable
        options={filmTitles}
        renderInput={(params) => (
          <TextField
            {...params}
            label="Search input"
            slotProps={{
              ...params.slotProps,
              input: {
                ...params.slotProps.input,
                type: 'search',
              },
            }}
          />
        )}
      />
      <Autocomplete
        freeSolo
        resetHighlightOnMouseLeave
        options={top100Films}
        renderInput={(params) => (
          <TextField {...params} label="freeSolo (handle string values)" />
        )}
        getOptionLabel={(option) =>
          typeof option === 'string' ? option : option.label
        }
        // A typed free solo value is a string, but a selected option is an object.
        // The equality check needs to compare both shapes.
        isOptionEqualToValue={(option, value) => {
          if (typeof value === 'string') {
            return option.label === value;
          }
          return option.label === value.label;
        }}
      />
    </Stack>
  );
}

```

:::warning
Free solo with non-string options can cause type mismatches. The typed value is always a string, so make sure your callbacks can handle both strings and option objects:

```tsx
<Autocomplete
  freeSolo
  options={options}
  getOptionLabel={(option) => (typeof option === 'string' ? option : option.label)}
/>
```

:::

### Creatable

To let users pick an existing option or create a new one, we recommend setting:

- `selectOnFocus`: highlight the input's current text when it receives focus so the user can overwrite it.
- `clearOnBlur`: clear leftover input text on blur when no option is picked or created.
- `handleHomeEndKeys`: move focus to the first or last option with <kbd class="key">Home</kbd> and <kbd class="key">End</kbd>.
- `resetHighlightOnMouseLeave`: clear mouse-created highlights when the pointer leaves the popup.
- A trailing option like `Add "${inputValue}"` to make the create action discoverable.

```tsx
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
import top100Films from './top100Films';

interface Film {
  label: string;
  year: number;
}

const filter = createFilterOptions<Film | string>();
const filmOptions: readonly Film[] = top100Films;

export default function FreeSoloCreateOption() {
  const [value, setValue] = React.useState<Film | string | null>(null);

  return (
    <Autocomplete<Film | string, false, false, true>
      value={value}
      onChange={(event, newValue) => {
        setValue(newValue);
      }}
      filterOptions={(options, params) => {
        const filtered = filter(options, params);

        const { inputValue, getOptionLabel } = params;
        // Suggest the creation of a new value
        const isExisting = options.some(
          (option) => inputValue === getOptionLabel(option),
        );
        if (inputValue !== '' && !isExisting) {
          filtered.push(inputValue);
        }

        return filtered;
      }}
      selectOnFocus
      clearOnBlur
      handleHomeEndKeys
      options={filmOptions}
      getOptionLabel={(option) => {
        if (typeof option === 'string') {
          return option;
        }
        return option.label;
      }}
      renderOption={(props, option) => {
        const { key, ...optionProps } = props;
        return (
          <li key={key} {...optionProps}>
            {typeof option === 'string' ? `Add "${option}"` : option.label}
          </li>
        );
      }}
      sx={{ width: 300 }}
      freeSolo
      resetHighlightOnMouseLeave
      renderInput={(params) => (
        <TextField {...params} label="Free solo with text demo" />
      )}
    />
  );
}

```

Or open a dialog when the user wants to add a new value.

```tsx
import * as React from 'react';
import TextField from '@mui/material/TextField';
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';
import Stack from '@mui/material/Stack';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
import top100Films from './top100Films';

interface Film {
  label: string;
  year: number;
}

const filter = createFilterOptions<Film | string>();
const filmOptions: readonly Film[] = top100Films;

export default function FreeSoloCreateOptionDialog() {
  const [value, setValue] = React.useState<Film | null>(null);
  const [open, toggleOpen] = React.useState(false);

  const handleClose = () => {
    setDialogValue({
      label: '',
      year: '',
    });
    toggleOpen(false);
  };

  const [dialogValue, setDialogValue] = React.useState({
    label: '',
    year: '',
  });

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setValue({
      label: dialogValue.label,
      year: Number.parseInt(dialogValue.year, 10),
    });
    handleClose();
  };

  const handleCreateOption = (inputValue: string) => {
    toggleOpen(true);
    setDialogValue({
      label: inputValue,
      year: '',
    });
  };

  return (
    <React.Fragment>
      <Autocomplete<Film | string, false, false, true>
        value={value}
        onChange={(event, newValue) => {
          if (typeof newValue === 'string') {
            // Avoid instant validation of the dialog's form.
            setTimeout(() => {
              handleCreateOption(newValue);
            });
          } else {
            setValue(newValue);
          }
        }}
        filterOptions={(options, params) => {
          const filtered = filter(options, params);

          const { inputValue, getOptionLabel } = params;
          const isExisting = options.some(
            (option) => inputValue === getOptionLabel(option),
          );

          if (inputValue !== '' && !isExisting) {
            filtered.push(inputValue);
          }

          return filtered;
        }}
        options={filmOptions}
        getOptionLabel={(option) => {
          if (typeof option === 'string') {
            return option;
          }
          return option.label;
        }}
        selectOnFocus
        clearOnBlur
        handleHomeEndKeys
        renderOption={(props, option) => {
          const { key, ...optionProps } = props;
          return (
            <li key={key} {...optionProps}>
              {typeof option === 'string' ? `Add "${option}"` : option.label}
            </li>
          );
        }}
        sx={{ width: 300 }}
        freeSolo
        resetHighlightOnMouseLeave
        renderInput={(params) => <TextField {...params} label="Free solo dialog" />}
      />
      <Dialog open={open} onClose={handleClose}>
        <form onSubmit={handleSubmit}>
          <DialogTitle>Add a new film</DialogTitle>
          <DialogContent>
            <DialogContentText>
              Did you miss any film in our list? Please, add it!
            </DialogContentText>
            <Stack spacing={2}>
              <TextField
                autoFocus
                value={dialogValue.label}
                onChange={(event) =>
                  setDialogValue({
                    ...dialogValue,
                    label: event.target.value,
                  })
                }
                label="title"
                type="text"
                variant="filled"
              />
              <TextField
                value={dialogValue.year}
                onChange={(event) =>
                  setDialogValue({
                    ...dialogValue,
                    year: event.target.value,
                  })
                }
                label="year"
                type="number"
                variant="filled"
              />
            </Stack>
          </DialogContent>
          <DialogActions>
            <Button onClick={handleClose}>Cancel</Button>
            <Button type="submit">Add</Button>
          </DialogActions>
        </form>
      </Dialog>
    </React.Fragment>
  );
}

```

## Multiple values

Set `multiple={true}` to let users select more than one value. By default, selected values render as removable Material UI Chips; customize their rendering with `renderValue`.

- Spread the props from `getItemProps` onto each rendered item to preserve the component's built-in behavior.
- If you replace the default Chip, destructure `onDelete` first; it's specific to `Chip`.

```tsx
import Chip from '@mui/material/Chip';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import top100Films from './top100Films';

const filmTitles = top100Films.map((option) => option.label);

export default function Tags() {
  return (
    <Stack spacing={3} sx={{ width: 500 }}>
      <Autocomplete
        multiple
        disableCloseOnSelect
        options={top100Films}
        defaultValue={[top100Films[13]]}
        renderInput={(params) => (
          <TextField {...params} label="Multiple values" placeholder="Favorites" />
        )}
      />
      <Autocomplete
        multiple
        disableCloseOnSelect
        options={top100Films}
        defaultValue={[top100Films[13]]}
        filterSelectedOptions
        renderInput={(params) => (
          <TextField
            {...params}
            label="filterSelectedOptions"
            placeholder="Favorites"
          />
        )}
      />
      <Autocomplete
        multiple
        disableCloseOnSelect
        options={filmTitles}
        defaultValue={[top100Films[13].label]}
        freeSolo
        renderValue={(value: readonly string[], getItemProps) =>
          value.map((option: string, index: number) => {
            const { key, ...itemProps } = getItemProps({ index });
            return (
              <Chip variant="outlined" label={option} key={key} {...itemProps} />
            );
          })
        }
        renderInput={(params) => (
          <TextField {...params} label="freeSolo" placeholder="Favorites" />
        )}
      />
      <Autocomplete
        multiple
        options={filmTitles}
        defaultValue={[top100Films[12].label, top100Films[13].label]}
        readOnly
        renderInput={(params) => (
          <TextField {...params} label="readOnly" placeholder="Favorites" />
        )}
      />
    </Stack>
  );
}

```

### Fixed options

To lock certain tags so they can't be removed, mark their chips as disabled.

```tsx
import * as React from 'react';
import Chip from '@mui/material/Chip';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import top100Films from './top100Films';

const fixedOptions = [top100Films[6]];

export default function FixedTags() {
  const [value, setValue] = React.useState([...fixedOptions, top100Films[13]]);

  return (
    <Autocomplete
      multiple
      disableCloseOnSelect
      value={value}
      onChange={(event, newValue) => {
        setValue([
          ...fixedOptions,
          ...newValue.filter((option) => !fixedOptions.includes(option)),
        ]);
      }}
      options={top100Films}
      renderValue={(values, getItemProps) =>
        values.map((option, index) => {
          const { key, ...itemProps } = getItemProps({ index });
          return (
            <Chip
              key={key}
              label={option.label}
              {...itemProps}
              disabled={fixedOptions.includes(option)}
            />
          );
        })
      }
      sx={{ width: 500 }}
      renderInput={(params) => (
        <TextField {...params} label="Fixed tag" placeholder="Favorites" />
      )}
    />
  );
}

```

### Selection indicators

Use icons as a visual cue for sighted users to show which options are selected.

```tsx
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import top100Films from './top100Films';

export default function CheckboxesTags() {
  return (
    <Autocomplete
      multiple
      options={top100Films}
      disableCloseOnSelect
      renderOption={(props, option, { selected }) => {
        const { key, ...optionProps } = props;
        const SelectionIcon = selected ? CheckBoxIcon : CheckBoxOutlineBlankIcon;

        return (
          <li key={key} {...optionProps}>
            <SelectionIcon
              fontSize="small"
              sx={{ mr: 1, p: 1.125, boxSizing: 'content-box' }}
            />
            {option.label}
          </li>
        );
      }}
      sx={{ width: 500 }}
      renderInput={(params) => (
        <TextField {...params} label="Checkboxes" placeholder="Favorites" />
      )}
    />
  );
}

```

### Limit tags

Use `limitTags` to limit how many selected items are visible when the input isn't focused.

```tsx
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import top100Films from './top100Films';

export default function LimitTags() {
  return (
    <Autocomplete
      multiple
      disableCloseOnSelect
      limitTags={2}
      options={top100Films}
      defaultValue={[top100Films[13], top100Films[12], top100Films[11]]}
      renderInput={(params) => (
        <TextField {...params} label="limitTags" placeholder="Favorites" />
      )}
      sx={{ width: 500 }}
    />
  );
}

```

## Asynchronous requests

The component supports two async patterns:

- [Load on open](#load-on-open): wait until the user interacts before fetching options.
- [Search as you type](#search-as-you-type): make a new request on every keystroke.

### Load on open

Shows a loading state while the request is pending.

```tsx
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
import useTimeout from '@mui/utils/useTimeout';
import top100Films from './top100Films';

const LOAD_DELAY = 1000;
const topFilms = top100Films.slice(0, 30);

export default function Asynchronous() {
  const loadOptions = useTimeout();
  const [open, setOpen] = React.useState(false);
  const [options, setOptions] = React.useState<readonly (typeof topFilms)[number][]>(
    [],
  );
  const [loading, setLoading] = React.useState(false);

  const handleOpen = () => {
    setOpen(true);
    setLoading(true);

    loadOptions.start(LOAD_DELAY, () => {
      setOptions([...topFilms]);
      setLoading(false);
    });
  };

  const handleClose = () => {
    loadOptions.clear();
    setOpen(false);
    setOptions([]);
    setLoading(false);
  };

  return (
    <Autocomplete
      sx={{ width: 300 }}
      open={open}
      onOpen={handleOpen}
      onClose={handleClose}
      options={options}
      loading={loading}
      renderInput={(params) => (
        <TextField
          {...params}
          label="Asynchronous"
          slotProps={{
            ...params.slotProps,
            input: {
              ...params.slotProps.input,
              endAdornment: (
                <React.Fragment>
                  {loading ? <CircularProgress color="inherit" size={20} /> : null}
                  {params.slotProps.input.endAdornment}
                </React.Fragment>
              ),
            },
          }}
        />
      )}
    />
  );
}

```

### Search as you type

If you fetch new options on every keystroke and filter on the server, throttle the requests.

Also disable the built-in client-side filtering—the server has already filtered the options, so re-filtering them would hide valid matches. Pass an identity function to `filterOptions`:

```jsx
<Autocomplete filterOptions={(x) => x} />
```

### Google Maps place

A customized UI on top of Google Places Autocomplete. The demo loads the [Google Maps JavaScript](https://developers.google.com/maps/documentation/javascript/overview) and [Google Places](https://developers.google.com/maps/documentation/places/web-service/overview) APIs.

```tsx
import * as React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Paper, { PaperProps } from '@mui/material/Paper';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import parse from 'autosuggest-highlight/parse';
// For this demo, debounce limits Google Maps Places API quota usage.
// import throttle from 'lodash/throttle';
import { debounce } from '@mui/material/utils';
import useId from '@mui/utils/useId';

// This key was created specifically for the demo in mui.com.
// You need to create a new one for your application.
const GOOGLE_MAPS_API_KEY = 'AIzaSyC3aviU6KHXAjoSnxcw6qbOhjnFctbxPkE';

const useEnhancedEffect =
  typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;

function loadScript(src: string, position: HTMLElement) {
  const script = document.createElement('script');
  script.setAttribute('async', '');
  script.src = src;
  position.appendChild(script);
  return script;
}

interface MainTextMatchedSubstrings {
  offset: number;
  length: number;
}
interface StructuredFormatting {
  main_text: string;
  main_text_matched_substrings: readonly MainTextMatchedSubstrings[];
  secondary_text?: string;
}
interface PlaceType {
  description: string;
  structured_formatting: StructuredFormatting;
}
interface AutocompleteSessionToken {}
interface PlacePredictionMatch {
  startOffset: number;
  endOffset: number;
}
interface PlacePredictionText {
  text: string;
  matches: readonly PlacePredictionMatch[];
}
interface PlacePrediction {
  text: { text: string };
  mainText: PlacePredictionText;
  secondaryText?: { text: string };
}
interface AutocompleteSuggestion {
  placePrediction: PlacePrediction;
}
interface GoogleMapsWindow extends Window {
  _google_callback?: Record<string, () => void>;
  google?: {
    maps: {
      places: {
        AutocompleteSessionToken: new () => AutocompleteSessionToken;
        AutocompleteSuggestion: {
          fetchAutocompleteSuggestions: (request: {
            input: string;
            sessionToken: AutocompleteSessionToken;
          }) => Promise<{ suggestions: readonly AutocompleteSuggestion[] }>;
        };
      };
    };
  };
}

function CustomPaper(props: PaperProps) {
  return (
    <Paper {...props}>
      {props.children}
      {/* Legal requirement https://developers.google.com/maps/documentation/javascript/policies#logo */}
      <Box
        sx={(staticTheme) => ({
          display: 'flex',
          justifyContent: 'flex-end',
          paddingBlockStart: 5,
          paddingBlockEnd: 6,
          paddingInline: 10,
          opacity: 0.9,
          '& path': {
            fill: '#5e5e5e',
          },
          ...staticTheme.applyStyles('dark', {
            opacity: 0.7,
            '& path': {
              fill: '#fff',
            },
          }),
        })}
      >
        <GoogleMapsLogo />
      </Box>
    </Paper>
  );
}

const emptyOptions: readonly PlaceType[] = [];
let sessionToken: AutocompleteSessionToken | null = null;

export default function GoogleMaps() {
  const [value, setValue] = React.useState<PlaceType | null>(null);
  const [inputValue, setInputValue] = React.useState('');
  const [options, setOptions] = React.useState<readonly PlaceType[]>(emptyOptions);
  const callbackId = useId();
  const scriptCallbackId = callbackId?.replace(/[^\w]/g, '');
  const [loaded, setLoaded] = React.useState(false);

  useEnhancedEffect(() => {
    if (!scriptCallbackId) {
      return;
    }

    const googleMapsWindow = window as GoogleMapsWindow;

    if (!document.querySelector('#google-maps')) {
      const GOOGLE_NAMESPACE = '_google_callback';
      const globalContext =
        googleMapsWindow[GOOGLE_NAMESPACE] ||
        (googleMapsWindow[GOOGLE_NAMESPACE] = {});
      globalContext[scriptCallbackId] = () => {
        setLoaded(true);
      };

      const script = loadScript(
        `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places&loading=async&callback=${GOOGLE_NAMESPACE}.${scriptCallbackId}`,
        document.querySelector('head')!,
      );
      script.id = 'google-maps';
    } else if (googleMapsWindow.google && !loaded) {
      setLoaded(true);
    }
  }, [loaded, scriptCallbackId]);

  useEnhancedEffect(() => {
    if (!loaded) {
      return undefined;
    }

    if (inputValue === '') {
      setOptions(value ? [value] : emptyOptions);
      return undefined;
    }

    // Ignore out-of-order responses.
    let active = true;
    const google = (window as GoogleMapsWindow).google;

    if (!google) {
      return undefined;
    }

    if (!sessionToken) {
      sessionToken = new google.maps.places.AutocompleteSessionToken();
    }

    fetchPredictions(
      { input: inputValue, sessionToken },
      (results?: readonly PlaceType[]) => {
        if (!active) {
          return;
        }

        let newOptions: readonly PlaceType[] = [];

        if (results) {
          newOptions = results;

          if (value) {
            newOptions = [
              value,
              ...results.filter(
                (result) => result.description !== value.description,
              ),
            ];
          }
        } else if (value) {
          newOptions = [value];
        }
        setOptions(newOptions);
      },
    );

    return () => {
      active = false;
    };
  }, [value, inputValue, loaded]);

  return (
    <Autocomplete
      sx={{ width: 300 }}
      getOptionLabel={(option) =>
        typeof option === 'string' ? option : option.description
      }
      filterOptions={(x) => x}
      slots={{
        paper: CustomPaper,
      }}
      options={options}
      autoComplete
      includeInputInList
      filterSelectedOptions
      resetHighlightOnMouseLeave
      value={value}
      noOptionsText="No locations"
      onChange={(event, newValue) => {
        setOptions(newValue ? [newValue, ...options] : options);
        setValue(newValue);
      }}
      onInputChange={(event, newInputValue) => {
        setInputValue(newInputValue);
      }}
      renderInput={(params) => (
        <TextField {...params} label="Add a location" fullWidth />
      )}
      renderOption={(props, option) => {
        const { key, ...optionProps } = props;
        const matches = option.structured_formatting.main_text_matched_substrings;

        const parts = parse(
          option.structured_formatting.main_text,
          matches.map((match: any) => [match.offset, match.offset + match.length]),
        );
        return (
          <li key={key} {...optionProps}>
            <Grid container sx={{ alignItems: 'center' }}>
              <Grid sx={{ display: 'flex', width: 44 }}>
                <LocationOnIcon sx={{ color: 'text.secondary' }} />
              </Grid>
              <Grid sx={{ width: 'calc(100% - 44px)', wordWrap: 'break-word' }}>
                {parts.map((part, index) => (
                  <Box
                    key={index}
                    component="span"
                    sx={{
                      fontWeight: part.highlight
                        ? 'fontWeightBold'
                        : 'fontWeightRegular',
                    }}
                  >
                    {part.text}
                  </Box>
                ))}
                {option.structured_formatting.secondary_text ? (
                  <Typography variant="body2" sx={{ color: 'text.secondary' }}>
                    {option.structured_formatting.secondary_text}
                  </Typography>
                ) : null}
              </Grid>
            </Grid>
          </li>
        );
      }}
    />
  );
}

// Fake data in case Google Maps Places API returns a rate limit.
const fakeAnswer = {
  p: [
    {
      description: 'Portugal',
      structured_formatting: {
        main_text: 'Portugal',
        main_text_matched_substrings: [{ offset: 0, length: 1 }],
      },
    },
    {
      description: 'Puerto Rico',
      structured_formatting: {
        main_text: 'Puerto Rico',
        main_text_matched_substrings: [{ offset: 0, length: 1 }],
      },
    },
    {
      description: 'Pakistan',
      structured_formatting: {
        main_text: 'Pakistan',
        main_text_matched_substrings: [{ offset: 0, length: 1 }],
      },
    },
    {
      description: 'Philippines',
      structured_formatting: {
        main_text: 'Philippines',
        main_text_matched_substrings: [{ offset: 0, length: 1 }],
      },
    },
    {
      description: 'Paris, France',
      structured_formatting: {
        main_text: 'Paris',
        main_text_matched_substrings: [{ offset: 0, length: 1 }],
        secondary_text: 'France',
      },
    },
  ],
  paris: [
    {
      description: 'Paris, France',
      structured_formatting: {
        main_text: 'Paris',
        main_text_matched_substrings: [{ offset: 0, length: 5 }],
        secondary_text: 'France',
      },
    },
    {
      description: 'Paris, TX, USA',
      structured_formatting: {
        main_text: 'Paris',
        main_text_matched_substrings: [{ offset: 0, length: 5 }],
        secondary_text: 'TX, USA',
      },
    },
    {
      description: "Paris Beauvais Airport, Route de l'Aéroport, Tillé, France",
      structured_formatting: {
        main_text: 'Paris Beauvais Airport',
        main_text_matched_substrings: [{ offset: 0, length: 5 }],
        secondary_text: "Route de l'Aéroport, Tillé, France",
      },
    },
    {
      description: 'Paris Las Vegas, South Las Vegas Boulevard, Las Vegas, NV, USA',
      structured_formatting: {
        main_text: 'Paris Las Vegas',
        main_text_matched_substrings: [{ offset: 0, length: 5 }],
        secondary_text: 'South Las Vegas Boulevard, Las Vegas, NV, USA',
      },
    },
    {
      description: "Paris La Défense Arena, Jardin de l'Arche, Nanterre, France",
      structured_formatting: {
        main_text: 'Paris La Défense Arena',
        main_text_matched_substrings: [{ offset: 0, length: 5 }],
        secondary_text: "Jardin de l'Arche, Nanterre, France",
      },
    },
  ],
};

const fetchPredictions = debounce(
  async (
    request: { input: string; sessionToken: AutocompleteSessionToken },
    callback: (results?: readonly PlaceType[]) => void,
  ) => {
    try {
      const google = (window as GoogleMapsWindow).google;
      if (!google) {
        return;
      }

      const { suggestions } =
        await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(
          request,
        );

      callback(
        suggestions.map((suggestion) => {
          const place = suggestion.placePrediction;
          // Map to the old AutocompleteService.getPlacePredictions format
          // https://developers.google.com/maps/documentation/javascript/places-migration-autocomplete
          return {
            description: place.text.text,
            structured_formatting: {
              main_text: place.mainText.text,
              main_text_matched_substrings: place.mainText.matches.map((match) => ({
                offset: match.startOffset,
                length: match.endOffset - match.startOffset,
              })),
              secondary_text: place.secondaryText?.text,
            },
          };
        }),
      );
    } catch (error: unknown) {
      if (
        error instanceof Error &&
        error.message.startsWith('Quota exceeded for quota')
      ) {
        callback(request.input.length === 1 ? fakeAnswer.p : fakeAnswer.paris);
        return;
      }

      throw error;
    }
  },
  400,
);

function GoogleMapsLogo() {
  return (
    <svg
      aria-label="Google Maps"
      height={14}
      preserveAspectRatio="xMidYMid meet"
      viewBox="0 0 98 18"
      width={77}
    >
      <path d="M7.08 13.96a6.9 6.9 0 01-4.99-2.05A6.7 6.7 0 010 6.98Q0 4.1 2.09 2.05A6.9 6.9 0 017.08 0a6.7 6.7 0 014.79 1.92l-1.35 1.35a4.8 4.8 0 00-3.44-1.36q-2.1 0-3.55 1.48a5 5 0 00-1.45 3.59q0 2.12 1.46 3.59a4.8 4.8 0 003.55 1.48 4.8 4.8 0 003.53-1.4q.84-.84 1.04-2.4H7.08v-1.9h6.42a6 6 0 01.1 1.19q0 2.8-1.65 4.46a6.4 6.4 0 01-4.87 1.96M22 12.68a4.4 4.4 0 01-3.2 1.29 4.4 4.4 0 01-3.2-1.29 4.3 4.3 0 01-1.31-3.21q0-1.92 1.31-3.21a4.4 4.4 0 013.2-1.29q1.9 0 3.2 1.29a4.3 4.3 0 011.31 3.21A4.3 4.3 0 0122 12.68m-4.99-1.26q.75.78 1.79.77 1.04 0 1.79-.77.75-.78.75-1.95 0-1.19-.74-1.96-.75-.77-1.8-.77t-1.8.77a2.7 2.7 0 00-.74 1.96q0 1.17.75 1.95m14.84 1.26q-1.3 1.29-3.2 1.29c-1.9 0-2.33-.43-3.2-1.29a4.3 4.3 0 01-1.31-3.21q0-1.92 1.31-3.21 1.3-1.29 3.2-1.29c1.9 0 2.33.43 3.2 1.29a4.3 4.3 0 011.31 3.21q0 1.92-1.31 3.21m-4.99-1.26q.75.78 1.79.77 1.04 0 1.79-.77.75-.78.75-1.95 0-1.19-.74-1.96c-.74-.77-1.09-.77-1.8-.77q-1.05 0-1.8.77a2.7 2.7 0 00-.74 1.96q0 1.17.75 1.95M38.32 18q-1.5 0-2.52-.8a4.5 4.5 0 01-1.46-1.86l1.72-.72q.27.65.85 1.12.59.48 1.41.48a2.3 2.3 0 001.76-.68q.64-.68.64-1.96v-.65h-.07a2.9 2.9 0 01-2.37 1.02 4 4 0 01-3.01-1.31 4.4 4.4 0 01-1.29-3.17 4.4 4.4 0 011.29-3.19 4 4 0 013.01-1.32q.76 0 1.39.29t.98.72h.07v-.72h1.87v8.07q0 2.35-1.2 3.52A4.2 4.2 0 0138.32 18m.13-5.81q1.02 0 1.71-.77a2.8 2.8 0 00.69-1.93q0-1.17-.69-1.96a2.2 2.2 0 00-1.71-.79q-1.03 0-1.77.78a2.8 2.8 0 00-.73 1.96q0 1.16.73 1.93.74.78 1.77.78M45.93.48v13.21h-1.98V.48zm5.41 13.48a4.38 4.38 0 01-4.46-4.49q0-1.98 1.23-3.24a4 4 0 013.01-1.26 3.8 3.8 0 012.68 1.07 5 5 0 011.17 1.8l.2.51-6.01 2.49a2.3 2.3 0 002.18 1.36q1.37 0 2.21-1.24l1.53 1.02q-.5.76-1.45 1.38-.92.6-2.29.6m-2.5-4.63l4.02-1.67a1.4 1.4 0 00-.63-.69 2 2 0 00-1.04-.26q-.87 0-1.63.72a2.4 2.4 0 00-.72 1.9m11.21 4.36V1.5h1.57l4.24 7.42h.07l4.24-7.42h1.57v12.19h-1.57V6.45l.07-2.04h-.07l-3.81 6.69h-.92l-3.81-6.69h-.07l.07 2.04v7.24zm16.31.27q-1.33 0-2.22-.77a2.5 2.5 0 01-.89-2.03q0-1.36 1.06-2.14 1.05-.77 2.61-.77 1.38 0 2.26.51v-.23q0-.91-.63-1.47A2.3 2.3 0 0077 6.51q-.68 0-1.23.32a1.6 1.6 0 00-.77.88l-1.43-.61q.28-.75 1.14-1.39a3.6 3.6 0 012.25-.64q1.6 0 2.66.94 1.05.93 1.06 2.64v5.04h-1.5v-1.16h-.08a3 3 0 01-2.74 1.43m.25-1.43q.97 0 1.76-.72.8-.72.79-1.71-.67-.54-1.99-.54-1.14 0-1.72.49-.58.5-.58 1.16 0 .61.53.97.54.35 1.21.35m9.97 1.43q-.96 0-1.71-.41a3 3 0 01-1.13-1.02h-.07l.07 1.16v3.68h-1.57V5.35h1.5v1.16h.07a3 3 0 011.13-1.02 3.67 3.67 0 014.5.87 4.5 4.5 0 011.18 3.17q0 1.9-1.18 3.17a3.7 3.7 0 01-2.79 1.26m-.26-1.43q1.1 0 1.87-.83.78-.82.78-2.19t-.78-2.19a2.5 2.5 0 00-1.87-.83q-1.11 0-1.88.82-.78.81-.77 2.2c.01 1.39.26 1.65.77 2.2q.78.82 1.88.82m8.39 1.43a3.8 3.8 0 01-3.65-2.38l1.4-.58q.67 1.57 2.26 1.57.73 0 1.2-.32a1 1 0 00.47-.85q0-.81-1.14-1.11l-1.69-.41a4 4 0 01-1.52-.77 1.9 1.9 0 01-.72-1.54q0-1.11.98-1.8a4 4 0 012.32-.69q1.11 0 1.98.5t1.24 1.44l-1.34.56q-.46-1.11-1.91-1.11-.7 0-1.18.29t-.48.78q0 .72 1.11.97l1.65.39a3 3 0 011.74.94q.56.66.56 1.5 0 1.12-.92 1.87-.9.75-2.36.75" />
    </svg>
  );
}

```

It uses [autosuggest-highlight](https://github.com/moroshko/autosuggest-highlight), a small (1 kB) utility for highlighting matched text.

:::error
You'll need your own [API key](https://developers.google.com/maps/documentation/javascript/get-api-key) to use the Google Maps and Places APIs.

This demo has a limited request quota. Once it's exceeded, results fall back to "Paris".
:::

### Infinite loading

Uses `@tanstack/react-query` to fetch more options when the user scrolls to the bottom of the list. The list is virtualized with `@tanstack/react-virtual`.

```tsx
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
import TextField from '@mui/material/TextField';
import {
  QueryClient,
  QueryClientProvider,
  useInfiniteQuery,
} from '@tanstack/react-query';
import { useEventCallback, useForkRef } from '@mui/material/utils';
import useTimeout from '@mui/utils/useTimeout';
import { useVirtualizer } from '@tanstack/react-virtual';
import type { Virtualizer } from '@tanstack/react-virtual';
import { fetchMovies, getMovieLabel, normalizeMovieQuery } from './server';
import type { Movie } from './movies';

const ITEM_HEIGHT_PX = 36;
const MAX_LISTBOX_HEIGHT_PX = 8 * ITEM_HEIGHT_PX;
const OVERSCAN = 5;
const PREFETCH_WITHIN_ITEMS = 5;
const INPUT_DEBOUNCE_MS = 200;

// Autocomplete invokes `renderOption(props, option)` for every option that
// would be rendered. Returning this tuple lets the virtual listbox own layout
// and mount only the rows that are visible.
type OptionTuple = readonly [
  React.HTMLAttributes<HTMLLIElement> & { key: React.Key },
  Movie,
];

type ListboxVirtualizer = Virtualizer<HTMLUListElement, Element>;

/** Props added to the Autocomplete listbox slot for infinite loading and virtualization. */
interface VirtualListboxProps extends React.HTMLAttributes<HTMLUListElement> {
  /** Called when the rendered window gets close enough to the end to load another page. */
  onReachEnd: () => void;
  /** Changes when the search context changes so the listbox can reset to the first row. */
  resetScrollKey: string;
  /** Exposes the virtualizer to the parent so keyboard navigation can scroll highlighted rows into view. */
  virtualizerRef: React.MutableRefObject<ListboxVirtualizer | null>;
}

/**
 * Virtualized Autocomplete listbox.
 * It mounts only visible options and triggers pagination as the rendered range approaches the end.
 */
const VirtualListbox = React.forwardRef<HTMLUListElement, VirtualListboxProps>(
  function VirtualListbox(props, forwardedRef) {
    const {
      children,
      onReachEnd,
      resetScrollKey,
      virtualizerRef,
      style,
      ...listboxProps
    } = props;
    const items = children as OptionTuple[];

    // One DOM node must serve both Autocomplete's listbox ref and the virtualizer's
    // scroll observer, so merge the forwarded ref with the local ref.
    const scrollContainerRef = React.useRef<HTMLUListElement | null>(null);
    const setScrollContainerRef = useForkRef(scrollContainerRef, forwardedRef);

    const virtualizer = useVirtualizer({
      count: items.length,
      getScrollElement: () => scrollContainerRef.current,
      estimateSize: () => ITEM_HEIGHT_PX,
      overscan: OVERSCAN,
      // Avoids forcing synchronous updates while Autocomplete is rendering.
      useFlushSync: false,
    });

    React.useEffect(() => {
      virtualizerRef.current = virtualizer;
      return () => {
        if (virtualizerRef.current === virtualizer) {
          virtualizerRef.current = null;
        }
      };
    }, [virtualizer, virtualizerRef]);

    React.useEffect(() => {
      scrollContainerRef.current?.scrollTo({ top: 0 });
      virtualizer.scrollToOffset(0);
    }, [resetScrollKey, virtualizer]);

    const virtualItems = virtualizer.getVirtualItems();
    const lastRenderedIndex = virtualItems[virtualItems.length - 1]?.index ?? -1;

    // Trigger pagination from the virtualizer's rendered range, not raw scroll
    // offsets, so overscan and keyboard scrolling behave consistently.
    React.useEffect(() => {
      if (
        items.length > 0 &&
        lastRenderedIndex >= items.length - PREFETCH_WITHIN_ITEMS
      ) {
        onReachEnd();
      }
    }, [lastRenderedIndex, items.length, onReachEnd]);

    return (
      <ul
        ref={setScrollContainerRef}
        {...listboxProps}
        style={{
          ...style,
          boxSizing: 'border-box',
          maxHeight: MAX_LISTBOX_HEIGHT_PX,
          overflow: 'auto',
          paddingBlock: 0,
          paddingInline: 0,
          margin: 0,
          position: 'relative',
          listStyle: 'none',
        }}
      >
        {/* This spacer gives the <ul> its scroll height without nesting a div inside the listbox. */}
        <li
          aria-hidden
          role="presentation"
          style={{
            height: virtualizer.getTotalSize(),
            pointerEvents: 'none',
          }}
        />
        {virtualItems.map((virtualItem) => {
          const [optionProps, option] = items[virtualItem.index];
          const { key, style: optionStyle, ...htmlProps } = optionProps;
          const label = getMovieLabel(option);

          return (
            <li
              key={key}
              {...htmlProps}
              style={{
                ...optionStyle,
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: virtualItem.size,
                transform: `translateY(${virtualItem.start}px)`,
              }}
            >
              <span
                title={label}
                style={{
                  display: 'block',
                  flexGrow: 1,
                  minWidth: 0,
                  overflow: 'hidden',
                  textOverflow: 'ellipsis',
                  whiteSpace: 'nowrap',
                }}
              >
                {label}
              </span>
            </li>
          );
        })}
      </ul>
    );
  },
);

function InfiniteQueryAutocomplete() {
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState<Movie | null>(null);
  const [inputValue, setInputValue] = React.useState('');
  const [searchInputValue, setSearchInputValue] = React.useState('');
  const [queryInputValue, setQueryInputValue] = React.useState('');
  const virtualizerRef = React.useRef<ListboxVirtualizer | null>(null);
  const wasOpenRef = React.useRef(false);
  const queryDebounce = useTimeout();

  React.useEffect(() => {
    // Opening the popup should query for whatever text is already visible in the
    // input, but reopening should not retrigger this sync while it is already open.
    if (open && !wasOpenRef.current) {
      setSearchInputValue(inputValue);
      setQueryInputValue(inputValue);
    }
    wasOpenRef.current = open;
  }, [inputValue, open]);

  React.useEffect(() => {
    if (!open || searchInputValue === queryInputValue) {
      queryDebounce.clear();
      return undefined;
    }

    queryDebounce.start(INPUT_DEBOUNCE_MS, () => {
      setQueryInputValue(searchInputValue);
    });

    return queryDebounce.clear;
  }, [open, queryDebounce, queryInputValue, searchInputValue]);

  const normalizedQuery = React.useMemo(
    () => normalizeMovieQuery(queryInputValue),
    [queryInputValue],
  );

  const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: ['movies', normalizedQuery],
      queryFn: ({ pageParam, signal }) =>
        fetchMovies(normalizedQuery, pageParam, signal),
      initialPageParam: 0,
      getNextPageParam: (lastPage) => lastPage.nextPage,
      enabled: open,
    });

  const options = React.useMemo(
    () => data?.pages.flatMap((page) => page.items) ?? [],
    [data],
  );

  const optionIndexMap = React.useMemo(() => {
    const indexMap = new Map<number, number>();

    options.forEach((option, index) => {
      indexMap.set(option.id, index);
    });

    return indexMap;
  }, [options]);

  const handleReachEnd = useEventCallback(() => {
    if (hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  });

  const handleInputChange = useEventCallback(
    (
      _event: React.SyntheticEvent,
      newInputValue: string,
      reason: 'input' | 'reset' | 'clear' | 'blur' | 'selectOption' | 'removeOption',
    ) => {
      setInputValue(newInputValue);

      // Autocomplete also calls `onInputChange` for selection and blur resets.
      // Only real typing should advance the remote query.
      if (reason === 'input') {
        setSearchInputValue(newInputValue);
      }

      if (reason === 'clear') {
        setSearchInputValue(newInputValue);
        setQueryInputValue(newInputValue);
      }
    },
  );

  const handleHighlightChange = useEventCallback(
    (_event: React.SyntheticEvent, option: Movie | null) => {
      const virtualizer = virtualizerRef.current;
      if (!option || !virtualizer) {
        return;
      }

      // Keep keyboard navigation aligned with virtualization. Autocomplete can
      // highlight rows that are not mounted, so its default scrollIntoView would
      // otherwise no-op for off-screen options.
      const index = optionIndexMap.get(option.id);
      if (index !== undefined) {
        virtualizer.scrollToIndex(index, { align: 'auto' });
      }
    },
  );

  // The listbox scrolls back to the top when the popup opens or the search
  // query changes, matching what users expect from a newly loaded result set.
  const listboxResetKey = open ? normalizedQuery : `closed:${normalizedQuery}`;

  return (
    <Autocomplete<Movie>
      sx={{ width: 320 }}
      open={open}
      onOpen={() => setOpen(true)}
      onClose={() => setOpen(false)}
      options={options}
      value={value}
      onChange={(_event, newValue) => setValue(newValue)}
      inputValue={inputValue}
      onInputChange={handleInputChange}
      // Results are already filtered by the query key, so disable the built-in
      // client filter.
      filterOptions={(x) => x}
      getOptionLabel={getMovieLabel}
      isOptionEqualToValue={(option, candidate) => option.id === candidate.id}
      loading={isFetching}
      loadingText="Loading movies…"
      disableListWrap
      onHighlightChange={handleHighlightChange}
      renderOption={(optionProps, option) =>
        [optionProps, option] as unknown as React.ReactNode
      }
      renderInput={(params) => {
        const { endAdornment, ...inputSlotProps } = params.slotProps.input;

        return (
          <TextField
            {...params}
            label="Movie"
            slotProps={{
              ...params.slotProps,
              input: {
                ...inputSlotProps,
                endAdornment: (
                  <React.Fragment>
                    {isFetching ? (
                      <CircularProgress color="inherit" size={18} />
                    ) : null}
                    {endAdornment}
                  </React.Fragment>
                ),
              },
            }}
          />
        );
      }}
      slotProps={{
        // The cast is only for the extra props injected into this custom slot.
        listbox: {
          component: VirtualListbox,
          onReachEnd: handleReachEnd,
          resetScrollKey: listboxResetKey,
          virtualizerRef,
        } as any,
      }}
    />
  );
}

const queryClientOptions = {
  defaultOptions: {
    queries: {
      staleTime: 60_000,
      refetchOnWindowFocus: false,
    },
  },
};

export default function InfiniteLoading() {
  // Create the QueryClient once for this mounted demo. In an app, this usually
  // lives near the root alongside the rest of your data-fetching setup.
  const [queryClient] = React.useState(() => new QueryClient(queryClientOptions));

  return (
    <QueryClientProvider client={queryClient}>
      <InfiniteQueryAutocomplete />
    </QueryClientProvider>
  );
}

```

## Customization

### Single value rendering

In the default single-selection mode (when `multiple={false}`), the selected option appears as plain text inside the input. Use `renderValue` to customize the display—for example, to add icons, badges, or formatted output.

The `renderValue` callback receives two parameters:

- `value`: the selected option to render inside the input.
- `getItemProps`: returns props for the rendered item. Forward the DOM-safe props you need, such as `className`, `data-item-index`, and `tabIndex`; omit `onDelete` unless you render a Chip.

```tsx
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';

type CardBrand = 'Visa' | 'Mastercard' | 'American Express' | 'Discover';

interface PaymentMethod {
  brand: CardBrand;
  number: string;
}

export default function CustomSingleValueRendering() {
  return (
    <Autocomplete
      sx={{ width: 360 }}
      options={paymentMethods}
      autoHighlight
      defaultValue={paymentMethods[0]}
      getOptionLabel={(option) =>
        `${option.brand} ending in ${getLastFourDigits(option.number)}`
      }
      isOptionEqualToValue={(option, value) => option.number === value.number}
      renderOption={(props, option) => {
        const { key, ...optionProps } = props;
        return (
          <Box key={key} component="li" {...optionProps}>
            <Box
              sx={{
                display: 'flex',
                justifyContent: 'space-between',
                gap: 2,
                width: '100%',
              }}
            >
              <span>{option.brand}</span>
              <Box component="span" sx={{ color: 'text.secondary' }}>
                *{getLastFourDigits(option.number)}
              </Box>
            </Box>
          </Box>
        );
      }}
      renderValue={(value, getItemProps) => {
        const itemProps = getItemProps();

        return (
          <Box
            component="span"
            className={itemProps.className}
            data-item-index={itemProps['data-item-index']}
            tabIndex={itemProps.tabIndex}
            aria-disabled={itemProps.disabled ? 'true' : undefined}
            sx={{
              display: 'inline-flex',
              alignItems: 'center',
              gap: 1,
              minWidth: 0,
              maxWidth: '100%',
            }}
          >
            <CardBrandIcon brand={value.brand} />
            <Box
              component="span"
              sx={{
                overflow: 'hidden',
                textOverflow: 'ellipsis',
                whiteSpace: 'nowrap',
              }}
            >
              {value.brand} ending in {getLastFourDigits(value.number)}
            </Box>
          </Box>
        );
      }}
      renderInput={(params) => <TextField {...params} label="Payment method" />}
    />
  );
}

function getLastFourDigits(cardNumber: string) {
  return cardNumber.slice(-4);
}

// Stripe test card numbers.
// https://docs.stripe.com/testing?testing-method=card-numbers#cards
const paymentMethods: readonly PaymentMethod[] = [
  {
    brand: 'Visa',
    number: '4242424242424242',
  },
  {
    brand: 'Mastercard',
    number: '5555555555554444',
  },
  {
    brand: 'American Express',
    number: '378282246310005',
  },
  {
    brand: 'Discover',
    number: '6011111111111117',
  },
];

function CardBrandIcon(props: { brand: CardBrand }) {
  const { brand } = props;

  if (brand === 'Visa') {
    return (
      <svg
        aria-hidden="true"
        focusable="false"
        height={22}
        viewBox="0 0 34 22"
        width={34}
      >
        <rect width="34" height="22" rx="3" fill="#fff" />
        <rect
          width="33"
          height="21"
          x="0.5"
          y="0.5"
          rx="2.5"
          fill="none"
          stroke="#d9dee7"
        />
        <text
          x="7"
          y="14.5"
          fill="#1434cb"
          fontFamily="Arial, sans-serif"
          fontSize="8"
          fontWeight="700"
          letterSpacing="0"
        >
          VISA
        </text>
      </svg>
    );
  }

  if (brand === 'Mastercard') {
    return (
      <svg
        aria-hidden="true"
        focusable="false"
        height={22}
        viewBox="0 0 34 22"
        width={34}
      >
        <rect width="34" height="22" rx="3" fill="#fff" />
        <rect
          width="33"
          height="21"
          x="0.5"
          y="0.5"
          rx="2.5"
          fill="none"
          stroke="#d9dee7"
        />
        <circle cx="14" cy="11" r="6" fill="#eb001b" />
        <circle cx="20" cy="11" r="6" fill="#f79e1b" fillOpacity="0.9" />
      </svg>
    );
  }

  if (brand === 'American Express') {
    return (
      <svg
        aria-hidden="true"
        focusable="false"
        height={22}
        viewBox="0 0 34 22"
        width={34}
      >
        <rect width="34" height="22" rx="3" fill="#2e77bc" />
        <path d="M0 14h34v5a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3z" fill="#1f5f9f" />
        <text
          x="6"
          y="13.5"
          fill="#fff"
          fontFamily="Arial, sans-serif"
          fontSize="7"
          fontWeight="700"
          letterSpacing="0"
        >
          AMEX
        </text>
      </svg>
    );
  }

  return (
    <svg
      aria-hidden="true"
      focusable="false"
      height={22}
      viewBox="0 0 34 22"
      width={34}
    >
      <rect width="34" height="22" rx="3" fill="#fff" />
      <rect
        width="33"
        height="21"
        x="0.5"
        y="0.5"
        rx="2.5"
        fill="none"
        stroke="#d9dee7"
      />
      <path d="M19 4a7 7 0 0 1 0 14h-5a7 7 0 0 0 0-14z" fill="#f58220" />
      <text
        x="4.5"
        y="13.5"
        fill="#111827"
        fontFamily="Arial, sans-serif"
        fontSize="5.6"
        fontWeight="700"
        letterSpacing="0"
      >
        DISC
      </text>
    </svg>
  );
}

```

### Highlights

Uses [autosuggest-highlight](https://github.com/moroshko/autosuggest-highlight) (1 kB) to highlight the matched portion of each option label.

```tsx
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import { alpha } from '@mui/material/styles';
import parse from 'autosuggest-highlight/parse';
import match from 'autosuggest-highlight/match';
import top100Films from './top100Films';

export default function Highlights() {
  return (
    <Autocomplete
      sx={{ width: 300 }}
      options={top100Films}
      renderInput={(params) => (
        <TextField {...params} label="Highlights" margin="normal" />
      )}
      renderOption={(props, option, { inputValue }) => {
        const { key, ...optionProps } = props;
        const matches = match(option.label, inputValue, { insideWords: true });
        const parts = parse(option.label, matches);

        return (
          <li key={key} {...optionProps}>
            <div>
              {parts.map((part, index) => (
                <Box
                  key={index}
                  component={part.highlight ? 'mark' : 'span'}
                  sx={(theme) => ({
                    backgroundColor: part.highlight
                      ? alpha(theme.palette.warning.main, 0.24)
                      : 'transparent',
                    borderRadius: 0.5,
                    color: 'inherit',
                    fontWeight: part.highlight ? 700 : 400,
                    paddingInline: part.highlight ? 0.25 : 0,
                  })}
                >
                  {part.text}
                </Box>
              ))}
            </div>
          </li>
        );
      }}
    />
  );
}

```

### Custom filter

Customize how options are filtered with `createFilterOptions`—a factory that returns a function suitable for the `filterOptions` prop.

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

#### `createFilterOptions(config) => filterOptions`

**Arguments**

1. `config` (_object_ [optional]):

- `config.ignoreAccents` (_bool_ [optional]): Defaults to `true`. Removes diacritics.
- `config.ignoreCase` (_bool_ [optional]): Defaults to `true`. Lowercases everything.
- `config.limit` (_number_ [optional]): Defaults to `null`. Limits how many matched options are shown. Useful when many options match and the listbox isn't virtualized.
- `config.matchFrom` (_'any' | 'start'_ [optional]): Defaults to `'any'`.
- `config.stringify` (_func_ [optional]): Controls how an option is converted to a string for matching against the input.
- `config.trim` (_bool_ [optional]): Defaults to `false`. Removes trailing spaces.

**Returns**

`filterOptions`: a function ready to pass to the `filterOptions` prop of `Autocomplete` (or the option of the same name in `useAutocomplete`).

In the demo below, options must start with the query string:

```jsx
const filterOptions = createFilterOptions({
  matchFrom: 'start',
  stringify: (option) => option.title,
});

<Autocomplete filterOptions={filterOptions} />;
```

```tsx
import TextField from '@mui/material/TextField';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
import top100Films from './top100Films';

const filterOptions = createFilterOptions<{ label: string; year: number }>({
  matchFrom: 'start',
  stringify: (option) => option.label,
});

export default function Filter() {
  return (
    <Autocomplete
      options={top100Films}
      filterOptions={filterOptions}
      sx={{ width: 300 }}
      renderInput={(params) => <TextField {...params} label="Custom filter" />}
    />
  );
}

```

### Advanced filter

For richer filtering—like fuzzy matching—we recommend [match-sorter](https://github.com/kentcdodds/match-sorter):

```jsx
import { matchSorter } from 'match-sorter';

const filterOptions = (options, { inputValue }) => matchSorter(options, inputValue);

<Autocomplete filterOptions={filterOptions} />;
```

### Sizes

Use the `size` prop to render a smaller input.

```tsx
import Stack from '@mui/material/Stack';
import Chip from '@mui/material/Chip';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import top100Films from './top100Films';

export default function Sizes() {
  return (
    <Stack spacing={2} sx={{ width: 300 }}>
      <Autocomplete
        size="small"
        options={top100Films}
        defaultValue={top100Films[13]}
        renderInput={(params) => (
          <TextField {...params} label="Size small" placeholder="Favorites" />
        )}
      />
      <Autocomplete
        multiple
        disableCloseOnSelect
        size="small"
        options={top100Films}
        defaultValue={[top100Films[13]]}
        renderInput={(params) => (
          <TextField {...params} label="Size small" placeholder="Favorites" />
        )}
      />
      <Autocomplete
        size="small"
        options={top100Films}
        defaultValue={top100Films[13]}
        renderInput={(params) => (
          <TextField
            {...params}
            variant="filled"
            label="Size small"
            placeholder="Favorites"
          />
        )}
      />
      <Autocomplete
        multiple
        disableCloseOnSelect
        size="small"
        options={top100Films}
        defaultValue={[top100Films[13]]}
        renderValue={(values, getItemProps) =>
          values.map((option, index) => {
            const { key, ...itemProps } = getItemProps({ index });
            return (
              <Chip
                key={key}
                variant="outlined"
                label={option.label}
                size="small"
                {...itemProps}
              />
            );
          })
        }
        renderInput={(params) => (
          <TextField
            {...params}
            variant="filled"
            label="Size small"
            placeholder="Favorites"
          />
        )}
      />
    </Stack>
  );
}

```

### HTML input attributes

When setting native input attributes on `TextField`—for example `maxLength`—merge them with `params.slotProps.htmlInput` instead of replacing the whole slot object.
Autocomplete passes its input ref and event handlers through that object, and dropping them can break focus, keyboard, and selection behavior.

```tsx
<Autocomplete
  options={options}
  renderInput={(params) => (
    <TextField
      {...params}
      slotProps={{
        ...params.slotProps,
        htmlInput: {
          ...params.slotProps.htmlInput,
          maxLength: 20,
          spellCheck: false,
        },
      }}
    />
  )}
/>
```

### Custom input

Customize the rendered input with the `renderInput` prop.
If you don't use `TextField`, attach `params.slotProps.input.ref` to the element that wraps the native input, and spread `params.slotProps.htmlInput` on the native input.

```tsx
<Autocomplete
  options={options}
  renderInput={(params) => (
    <div ref={params.slotProps.input.ref}>
      <input type="text" {...params.slotProps.htmlInput} />
    </div>
  )}
/>
```

### Globally customized options

To customize option rendering for every Autocomplete in your app, set `renderOption` in [theme default props](/material-ui/customization/theme-components/#theme-default-props).

`renderOption` receives `ownerState` as its fourth argument, which exposes props and internal state. Use `ownerState.getOptionLabel` to render the label.

This keeps option styling consistent across the app while letting each instance customize its content.

```tsx
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import type { Theme } from '@mui/material/styles';
import countries from './countries';

interface Option {
  label: string;
  detail: string;
}

const getOptionKey = (option: Option) => `${option.label}-${option.detail}`;

// Theme.ts
const customTheme = (outerTheme: Theme) =>
  createTheme({
    ...outerTheme,
    components: {
      MuiAutocomplete: {
        defaultProps: {
          renderOption: (props, option, state, ownerState) => {
            const { key, ...optionProps } = props;
            const { detail } = option as Option;

            return (
              <Box
                key={key}
                sx={{
                  borderRadius: 1,
                  m: 0.625,
                  [`&.${autocompleteClasses.option}`]: {
                    display: 'flex',
                    alignItems: 'center',
                    gap: 2,
                    p: 1,
                  },
                }}
                component="li"
                {...optionProps}
              >
                <Box
                  component="span"
                  sx={{
                    minWidth: 0,
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                    whiteSpace: 'nowrap',
                  }}
                >
                  {ownerState.getOptionLabel(option)}
                </Box>
                <Chip
                  label={detail}
                  size="small"
                  sx={{
                    ml: 'auto',
                    flexShrink: 0,
                  }}
                />
              </Box>
            );
          },
        },
      },
    },
  });

const languages: readonly Option[] = [
  { detail: 'en-US', label: 'English (US)' },
  { detail: 'en-GB', label: 'English (UK)' },
  { detail: 'es-ES', label: 'Spanish (Spain)' },
  { detail: 'es-MX', label: 'Spanish (Mexico)' },
  { detail: 'fr-FR', label: 'French' },
  { detail: 'de-DE', label: 'German' },
  { detail: 'it-IT', label: 'Italian' },
  { detail: 'ja-JP', label: 'Japanese' },
  { detail: 'ko-KR', label: 'Korean' },
  { detail: 'pt-BR', label: 'Portuguese' },
  { detail: 'zh-CN', label: 'Chinese (Simplified)' },
  { detail: 'zh-TW', label: 'Chinese (Traditional)' },
];

const countryOptions: readonly Option[] = countries.map((country) => ({
  label: `${country.label} (${country.code})`,
  detail: `+${country.countryCallingCode}`,
}));

export default function GloballyCustomizedOptions() {
  return (
    <ThemeProvider theme={customTheme}>
      <Stack spacing={2} sx={{ width: 300 }}>
        <LanguageSelect />
        <CountrySelect />
      </Stack>
    </ThemeProvider>
  );
}

function LanguageSelect() {
  return (
    <Autocomplete
      options={languages}
      getOptionLabel={(option) => option.label}
      renderInput={(params) => (
        <TextField {...params} label="Preferred language" variant="filled" />
      )}
    />
  );
}

function CountrySelect() {
  return (
    <Autocomplete
      options={countryOptions}
      getOptionLabel={(option) => option.label}
      getOptionKey={getOptionKey}
      renderInput={(params) => <TextField {...params} label="Choose a country" />}
    />
  );
}

```

### GitHub's picker

A reproduction of GitHub's label picker:

```tsx
import * as React from 'react';
import useId from '@mui/utils/useId';
import { alpha, styled } from '@mui/material/styles';
import Popper from '@mui/material/Popper';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import ButtonBase from '@mui/material/ButtonBase';
import InputBase from '@mui/material/InputBase';
import Box from '@mui/material/Box';
import Autocomplete, {
  AutocompleteCloseReason,
  autocompleteClasses,
} from '@mui/material/Autocomplete';

interface IssueLabel {
  name: string;
  color: string;
  description?: string;
}

interface PopperComponentProps {
  anchorEl?: unknown;
  disablePortal?: boolean;
  open: boolean;
  style?: React.CSSProperties;
}

const TriggerButton = styled(ButtonBase)(({ theme }) => ({
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'space-between',
  width: '100%',
  paddingBlock: 5,
  paddingInline: 8,
  borderRadius: 6,
  fontSize: 14,
  fontWeight: 600,
  fontFamily: 'inherit',
  textAlign: 'left',
  color: primer.light.fgMuted,
  transition: 'background-color 80ms',
  ...theme.applyStyles('dark', { color: primer.dark.fgMuted }),
  '& svg': { color: 'currentColor' },
  '&:hover': {
    backgroundColor: primer.light.transparentHover,
    ...theme.applyStyles('dark', { backgroundColor: primer.dark.transparentHover }),
  },
  '&[aria-expanded="true"]': {
    backgroundColor: primer.light.transparentActive,
    ...theme.applyStyles('dark', { backgroundColor: primer.dark.transparentActive }),
  },
  '&:focus-visible': {
    outlineWidth: 2,
    outlineStyle: 'solid',
    outlineColor: primer.light.fgAccent,
    outlineOffset: -2,
    ...theme.applyStyles('dark', { outlineColor: primer.dark.fgAccent }),
  },
}));

const Panel = styled(Popper)(({ theme }) => ({
  zIndex: theme.zIndex.modal,
  width: PANEL_WIDTH,
  maxWidth: `calc(100vw - ${theme.spacing(4)})`,
  borderRadius: 12,
  borderWidth: 1,
  borderStyle: 'solid',
  borderColor: primer.light.borderMuted,
  boxShadow: `0 ${theme.spacing(1)} ${theme.spacing(3)} ${primer.light.overlayShadowColor}`,
  backgroundColor: primer.light.bgDefault,
  color: primer.light.fgDefault,
  fontSize: 14,
  ...theme.applyStyles('dark', {
    borderColor: primer.dark.borderMuted,
    boxShadow: `0 ${theme.spacing(1)} ${theme.spacing(3)} ${primer.dark.overlayShadowColor}`,
    backgroundColor: primer.dark.bgDefault,
    color: primer.dark.fgDefault,
  }),
}));

const SearchField = styled('div')(({ theme }) => ({
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  gap: 8,
  minHeight: 32,
  paddingBlock: 0,
  paddingInline: 8,
  borderRadius: 6,
  backgroundColor: primer.light.bgInset,
  borderWidth: 1,
  borderStyle: 'solid',
  borderColor: primer.light.borderMuted,
  color: primer.light.fgMuted,
  fontSize: 14,
  lineHeight: 20 / 14,
  ...theme.applyStyles('dark', {
    backgroundColor: primer.dark.bgInset,
    borderColor: primer.dark.borderMuted,
    color: primer.dark.fgMuted,
  }),
  '&:focus-within': {
    borderColor: primer.light.fgAccent,
    outlineWidth: 2,
    outlineStyle: 'solid',
    outlineColor: primer.light.fgAccent,
    outlineOffset: -1,
    ...theme.applyStyles('dark', {
      borderColor: primer.dark.fgAccent,
      outlineColor: primer.dark.fgAccent,
    }),
  },
  '& input': {
    fontSize: 14,
    color: primer.light.fgDefault,
    ...theme.applyStyles('dark', { color: primer.dark.fgDefault }),
    '&::placeholder': {
      color: primer.light.fgMuted,
      opacity: 1,
      ...theme.applyStyles('dark', { color: primer.dark.fgMuted }),
    },
  },
}));

// `width: 100%` keeps the listbox flush with the panel; without it, popper.js's
// inline positioning style would constrain the wrapper and leave a gap to the
// right of the scrollbar.
const ListboxContainer = styled('div')(({ theme }) => ({
  width: '100%',
  [`& .${autocompleteClasses.paper}`]: {
    margin: 0,
    color: 'inherit',
    backgroundColor: 'transparent',
    boxShadow: 'none',
  },
  [`& .${autocompleteClasses.listbox}`]: {
    // Vertical-only padding so the listbox's content area is full-width.
    // Horizontal inset is applied via `margin-inline` on each option.
    paddingBlock: 8,
    paddingInline: 0,
    maxHeight: LISTBOX_MAX_HEIGHT,
    backgroundColor: 'transparent',
    [`& .${autocompleteClasses.option}`]: {
      position: 'relative',
      marginInline: 8,
      borderRadius: 6,
      paddingBlock: 6,
      paddingInline: 8,
      gap: 8,
      alignItems: 'flex-start',
      // Let the focus indicator extend into the listbox gutter.
      overflow: 'visible',
      '&:not(:first-of-type)::before': {
        content: '""',
        position: 'absolute',
        top: 0,
        left: 8,
        right: 8,
        height: 1,
        backgroundColor: primer.light.borderMuted,
        ...theme.applyStyles('dark', { backgroundColor: primer.dark.borderMuted }),
      },
      '&[aria-selected="true"]': {
        backgroundColor: 'transparent',
      },
      [`&.${autocompleteClasses.focused}, &.${autocompleteClasses.focused}[aria-selected="true"]`]:
        {
          backgroundColor: primer.light.transparentHover,
          ...theme.applyStyles('dark', {
            backgroundColor: primer.dark.transparentHover,
          }),
        },
      [`&.${autocompleteClasses.focused}::after`]: {
        content: '""',
        position: 'absolute',
        top: 6,
        bottom: 6,
        left: -8,
        width: 3,
        borderRadius: 6,
        backgroundColor: primer.light.fgAccent,
        ...theme.applyStyles('dark', { backgroundColor: primer.dark.fgAccent }),
      },
      [`&.${autocompleteClasses.focused}::before, &.${autocompleteClasses.focused} + li::before`]:
        {
          visibility: 'hidden',
        },
    },
  },
  [`&.${autocompleteClasses.popperDisablePortal}`]: {
    position: 'relative',
  },
}));

function ListboxPopper(props: PopperComponentProps) {
  // Discard popper.js positioning props — we render inline within the panel.
  const { disablePortal, anchorEl, open, style, ...other } = props;
  return <ListboxContainer {...other} />;
}

const OptionIndicator = styled('span')(({ theme }) => ({
  position: 'relative',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  flexShrink: 0,
  width: 16,
  height: 16,
  marginTop: 4,
  borderRadius: 4,
  backgroundColor: primer.light.bgDefault,
  borderWidth: 1,
  borderStyle: 'solid',
  borderColor: primer.light.borderEmphasis,
  ...theme.applyStyles('dark', {
    backgroundColor: primer.dark.bgDefault,
    borderColor: primer.dark.borderEmphasis,
  }),
  '& svg': {
    color: '#ffffff',
    visibility: 'hidden',
  },
  '&[data-checked]': {
    backgroundColor: primer.light.fgAccent,
    borderColor: primer.light.fgAccent,
    ...theme.applyStyles('dark', {
      backgroundColor: primer.dark.fgAccent,
      borderColor: primer.dark.fgAccent,
    }),
    '& svg': {
      visibility: 'visible',
    },
  },
}));

// Pin already-selected items to the top of the list.
function selectedFirst(all: readonly IssueLabel[], selected: readonly IssueLabel[]) {
  const order = (label: IssueLabel) => {
    const i = selected.indexOf(label);
    return i === -1 ? selected.length + all.indexOf(label) : i;
  };
  return [...all].sort((a, b) => order(a) - order(b));
}

function isLightLabel(color: string) {
  const red = Number.parseInt(color.slice(1, 3), 16);
  const green = Number.parseInt(color.slice(3, 5), 16);
  const blue = Number.parseInt(color.slice(5, 7), 16);
  const perceivedLightness = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255;
  return perceivedLightness > 0.96;
}

function LabelPill({ label }: { label: IssueLabel }) {
  return (
    <Box
      component="span"
      sx={(theme) => ({
        display: 'inline-block',
        boxSizing: 'border-box',
        py: 0,
        px: 0.875,
        fontSize: 12,
        fontWeight: 500,
        lineHeight: 18 / 12,
        whiteSpace: 'nowrap',
        verticalAlign: 'middle',
        borderRadius: '2em',
        borderWidth: 1,
        borderStyle: 'solid',
        color: theme.palette.getContrastText(label.color),
        backgroundColor: label.color,
        borderColor: isLightLabel(label.color)
          ? 'rgba(31,35,40,0.15)'
          : 'transparent',
        ...theme.applyStyles('dark', {
          color: theme.lighten(label.color, 0.45),
          backgroundColor: alpha(label.color, 0.18),
          borderColor: alpha(label.color, 0.3),
        }),
      })}
    >
      {label.name}
    </Box>
  );
}

export default function GitHubLabel() {
  const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
  // `value` is the committed selection rendered in the sidebar.
  // `pendingValue` tracks edits while the picker is open and commits on close.
  const [value, setValue] = React.useState<IssueLabel[]>([labels[0], labels[4]]);
  const [pendingValue, setPendingValue] = React.useState<IssueLabel[]>([]);

  const open = Boolean(anchorEl);
  const panelId = useId();
  const panelTitleId = panelId ? `${panelId}-title` : undefined;

  // `restoreFocus` returns focus to the trigger button — appropriate for
  // keyboard dismissal (Escape), but not for click-away or tab-out, which
  // should leave focus on whatever the user moved it to.
  const handleClose = ({ restoreFocus = false } = {}) => {
    setValue(pendingValue);
    if (restoreFocus) {
      anchorEl?.focus();
    }
    setAnchorEl(null);
  };

  const handleToggle = (event: React.MouseEvent<HTMLElement>) => {
    if (open) {
      handleClose();
    } else {
      setPendingValue(value);
      setAnchorEl(event.currentTarget);
    }
  };

  const handleClickAway = (event: MouseEvent | TouchEvent) => {
    if (anchorEl?.contains(event.target as Node)) {
      return;
    }
    handleClose();
  };

  return (
    <React.Fragment>
      <Box
        sx={(theme) => ({
          width: TRIGGER_WIDTH,
          pb: 1.5,
          borderBottom: 1,
          borderColor: primer.light.borderMuted,
          ...theme.applyStyles('dark', {
            borderColor: primer.dark.borderMuted,
          }),
        })}
      >
        <TriggerButton
          aria-controls={open ? panelId : undefined}
          aria-haspopup="dialog"
          aria-expanded={open}
          disableRipple
          onClick={handleToggle}
        >
          Labels
          <GearIcon />
        </TriggerButton>
        <Box
          sx={{
            display: 'flex',
            flexWrap: 'wrap',
            gap: 0.5,
            mt: 1,
            pl: 1,
          }}
        >
          {value.length === 0 ? (
            <Box
              sx={(theme) => ({
                fontSize: 12,
                color: primer.light.fgMuted,
                ...theme.applyStyles('dark', { color: primer.dark.fgMuted }),
              })}
            >
              None yet
            </Box>
          ) : (
            value.map((label) => <LabelPill key={label.name} label={label} />)
          )}
        </Box>
      </Box>

      <Panel
        open={open}
        anchorEl={anchorEl}
        placement="bottom-start"
        modifiers={[{ name: 'offset', options: { offset: [0, 8] } }]}
      >
        <ClickAwayListener onClickAway={handleClickAway}>
          <div id={panelId} role="dialog" aria-labelledby={panelTitleId}>
            <Box
              id={panelTitleId}
              component="h2"
              sx={(theme) => ({
                margin: 0,
                pt: 1.5,
                px: 2,
                pb: 1,
                fontSize: 14,
                fontWeight: 600,
                borderBottom: 1,
                borderColor: primer.light.borderMuted,
                ...theme.applyStyles('dark', {
                  borderColor: primer.dark.borderMuted,
                }),
              })}
            >
              Apply labels
            </Box>
            <Autocomplete
              open
              multiple
              disableCloseOnSelect
              renderValue={() => null}
              value={pendingValue}
              options={selectedFirst(labels, value)}
              getOptionLabel={(option) => option.name}
              noOptionsText="No labels"
              onClose={(_event, reason: AutocompleteCloseReason) => {
                if (reason === 'escape') {
                  handleClose({ restoreFocus: true });
                } else if (reason === 'blur') {
                  handleClose();
                }
              }}
              onChange={(event, newValue, reason) => {
                // Backspace/Delete on a focused chip can trigger removeOption,
                // but we render no chips, so ignore those events.
                if (
                  event.type === 'keydown' &&
                  reason === 'removeOption' &&
                  ((event as React.KeyboardEvent).key === 'Backspace' ||
                    (event as React.KeyboardEvent).key === 'Delete')
                ) {
                  return;
                }
                setPendingValue(newValue);
              }}
              renderOption={(props, option, { selected }) => {
                const { key, ...optionProps } = props;
                return (
                  <li key={key} {...optionProps}>
                    <OptionIndicator data-checked={selected || undefined}>
                      <CheckIcon />
                    </OptionIndicator>
                    <Box
                      component="span"
                      sx={{
                        width: 14,
                        height: 14,
                        mt: 0.625,
                        flexShrink: 0,
                        borderRadius: 1.5,
                      }}
                      style={{ backgroundColor: option.color }}
                    />
                    <Box sx={{ flexGrow: 1, minWidth: 0 }}>
                      <Box sx={{ fontWeight: 500 }}>{option.name}</Box>
                      {option.description && (
                        <Box
                          sx={(theme) => ({
                            fontSize: 12,
                            color: primer.light.fgMuted,
                            ...theme.applyStyles('dark', {
                              color: primer.dark.fgMuted,
                            }),
                          })}
                        >
                          {option.description}
                        </Box>
                      )}
                    </Box>
                  </li>
                );
              }}
              renderInput={(params) => {
                const { ref, className, onMouseDown } = params.slotProps.input;

                return (
                  <Box sx={{ p: 1 }}>
                    <SearchField
                      ref={ref}
                      className={className}
                      onMouseDown={onMouseDown}
                    >
                      <SearchIcon />
                      <InputBase
                        inputProps={{
                          ...params.slotProps.htmlInput,
                          'aria-label': 'Filter labels',
                        }}
                        autoFocus
                        placeholder="Filter labels"
                        sx={{
                          flex: 1,
                          minWidth: 0,
                          fontSize: 14,
                          padding: 0,
                          color: 'inherit',
                        }}
                      />
                    </SearchField>
                  </Box>
                );
              }}
              slots={{ popper: ListboxPopper }}
            />
          </div>
        </ClickAwayListener>
      </Panel>
    </React.Fragment>
  );
}

// GitHub's default issue labels (created on every new repository),
// plus a few common community additions.
const labels: IssueLabel[] = [
  { name: 'bug', color: '#d73a4a', description: "Something isn't working" },
  {
    name: 'documentation',
    color: '#0075ca',
    description: 'Improvements or additions to documentation',
  },
  {
    name: 'duplicate',
    color: '#cfd3d7',
    description: 'This issue or pull request already exists',
  },
  {
    name: 'enhancement',
    color: '#a2eeef',
    description: 'New feature or request',
  },
  {
    name: 'good first issue',
    color: '#7057ff',
    description: 'Good for newcomers',
  },
  {
    name: 'help wanted',
    color: '#008672',
    description: 'Extra attention is needed',
  },
  { name: 'invalid', color: '#e4e669', description: "This doesn't seem right" },
  {
    name: 'question',
    color: '#d876e3',
    description: 'Further information is requested',
  },
  { name: 'wontfix', color: '#cfd3d7', description: 'This will not be worked on' },
  { name: 'breaking change', color: '#ee0701' },
  { name: 'needs triage', color: '#d4c5f9' },
  { name: 'security', color: '#b60205' },
];

const primer = {
  light: {
    borderMuted: '#d1d9e0',
    borderEmphasis: '#818b98',
    fgMuted: '#59636e',
    fgDefault: '#1f2328',
    fgAccent: '#0969da',
    bgDefault: '#ffffff',
    bgInset: '#f6f8fa',
    transparentHover: 'rgba(208,215,222,0.32)',
    transparentActive: 'rgba(208,215,222,0.48)',
    overlayShadowColor: 'rgba(140,149,159,0.2)',
  },
  dark: {
    borderMuted: '#3d444d',
    borderEmphasis: '#7d8590',
    fgMuted: '#9198a1',
    fgDefault: '#f0f6fc',
    fgAccent: '#1f6feb',
    bgDefault: '#0d1117',
    bgInset: '#010409',
    transparentHover: 'rgba(101,108,118,0.18)',
    transparentActive: 'rgba(101,108,118,0.32)',
    overlayShadowColor: 'rgba(1,4,9,0.85)',
  },
} as const;

const TRIGGER_WIDTH = 240;
const PANEL_WIDTH = 320;
const LISTBOX_MAX_HEIGHT = 320;

function GearIcon() {
  return (
    <svg viewBox="0 0 16 16" width={16} height={16} fill="currentColor" aria-hidden>
      <path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.037.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z" />
    </svg>
  );
}

function CheckIcon() {
  return (
    <svg viewBox="0 0 16 16" width={12} height={12} fill="currentColor" aria-hidden>
      <path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
    </svg>
  );
}

function SearchIcon() {
  return (
    <svg viewBox="0 0 16 16" width={16} height={16} fill="currentColor" aria-hidden>
      <path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.998 0A4.499 4.499 0 0 0 11.5 7Z" />
    </svg>
  );
}

```

### Hint

Add a hint (ghost text suggestion) inside the input:

```tsx
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import top100Films from './top100Films';

export default function AutocompleteHint() {
  const [inputValue, setInputValue] = React.useState('');
  const [hint, setHint] = React.useState('');

  return (
    <Autocomplete
      onKeyDown={(event) => {
        if (event.key === 'Tab' && hint) {
          setInputValue(hint);
          setHint('');
          event.preventDefault();
        }
      }}
      onClose={() => {
        setHint('');
      }}
      onChange={(event, newValue) => {
        setInputValue(newValue?.label ?? '');
        setHint('');
      }}
      onInputChange={(event, newInputValue, reason) => {
        setInputValue(newInputValue);

        if (reason !== 'input') {
          setHint('');
          return;
        }

        const matchingOption = top100Films.find((option) =>
          option.label.startsWith(newInputValue),
        );

        setHint(newInputValue && matchingOption ? matchingOption.label : '');
      }}
      disablePortal
      resetHighlightOnMouseLeave
      inputValue={inputValue}
      options={top100Films}
      sx={{ width: 300 }}
      renderInput={(params) => {
        return (
          <Box sx={{ position: 'relative' }}>
            <Typography
              aria-hidden="true"
              sx={{
                position: 'absolute',
                opacity: 0.5,
                left: 14,
                right: 61,
                top: 16,
                overflow: 'hidden',
                whiteSpace: 'nowrap',
              }}
            >
              {hint}
            </Typography>
            <TextField {...params} label="Movie" />
          </Box>
        );
      }}
    />
  );
}

```

### Events

Callbacks such as `onChange`, `onClose`, `onHighlightChange`, and `onInputChange` include a `reason` argument. Use it to distinguish user input from selection, clear, and other internal updates:

- Selected value changes use `AutocompleteChangeReason`, covering selection, creation, removal, clear, and blur transitions.
- Textbox changes use `AutocompleteInputChangeReason`, which separates user typing from resets, clears, blur, and option selection or removal.
- Popup and highlight changes use `AutocompleteCloseReason` and `AutocompleteHighlightChangeReason` to describe why the popup closed or how the highlighted option moved.

```jsx
<Autocomplete
  onInputChange={(_event, value, reason) => {
    if (reason === 'input') {
      setQuery(value);
    }
  }}
/>
```

To override the default key handling, set `defaultMuiPrevented` to `true` on the event:

```jsx
<Autocomplete
  onKeyDown={(event) => {
    if (event.key === 'Enter') {
      // Prevents the default 'Enter' behavior.
      event.defaultMuiPrevented = true;
      // your handler code
    }
  }}
/>
```

### Virtualization

Searches through a fixed list of 10,000 randomly generated options. The list is virtualized with [react-window](https://github.com/bvaughn/react-window).

```tsx
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete';
import useMediaQuery from '@mui/material/useMediaQuery';
import Popper from '@mui/material/Popper';
import { useTheme, styled } from '@mui/material/styles';
import {
  List,
  RowComponentProps,
  useListRef,
  ListImperativeAPI,
} from 'react-window';
import Typography from '@mui/material/Typography';

const LISTBOX_PADDING = 8; // px

type ItemData = Array<
  [React.HTMLAttributes<HTMLLIElement> & { key: React.Key }, string, number]
>;

function RowComponent({
  index,
  itemData,
  style,
}: RowComponentProps & {
  itemData: ItemData;
}) {
  const dataSet = itemData[index];
  const inlineStyle = {
    ...style,
    top: ((style.top as number) ?? 0) + LISTBOX_PADDING,
  };

  const { key, ...optionProps } = dataSet[0];

  return (
    <Typography
      key={key}
      component="li"
      {...optionProps}
      aria-posinset={dataSet[2] + 1}
      aria-setsize={itemData.length}
      noWrap
      style={inlineStyle}
    >
      {`#${dataSet[2] + 1} - ${dataSet[1]}`}
    </Typography>
  );
}

// Adapter for react-window v2
const ListboxComponent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLElement> & {
    internalListRef: React.Ref<ListImperativeAPI>;
    onItemsBuilt: (optionIndexMap: Map<string, number>) => void;
  }
>(function ListboxComponent(props, ref) {
  const { children, internalListRef, onItemsBuilt, ...other } = props;
  const itemData = children as ItemData;

  const optionIndexMap = React.useMemo(() => {
    const map = new Map<string, number>();

    itemData.forEach((item, index) => {
      map.set(item[1], index);
    });

    return map;
  }, [itemData]);

  React.useEffect(() => {
    if (onItemsBuilt) {
      onItemsBuilt(optionIndexMap);
    }
  }, [onItemsBuilt, optionIndexMap]);

  const theme = useTheme();
  const smUp = useMediaQuery(theme.breakpoints.up('sm'), {
    noSsr: true,
  });
  const itemCount = itemData.length;
  const itemSize = smUp ? 36 : 48;

  const getHeight = () => {
    if (itemCount > 8) {
      return 8 * itemSize;
    }
    return itemCount * itemSize;
  };

  // Separate className for List, other props for wrapper div (ARIA, handlers)
  const { className, style, ...otherProps } = other;

  return (
    <div ref={ref} {...otherProps}>
      <List
        className={className}
        listRef={internalListRef}
        key={itemCount}
        rowCount={itemCount}
        rowHeight={itemSize}
        rowComponent={RowComponent}
        rowProps={{ itemData }}
        style={{
          height: getHeight() + 2 * LISTBOX_PADDING,
          width: '100%',
        }}
        overscanCount={5}
        tagName="ul"
      />
    </div>
  );
});

function random(length: number) {
  const characters =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';

  for (let i = 0; i < length; i += 1) {
    result += characters.charAt(Math.floor(Math.random() * characters.length));
  }

  return result;
}

const StyledPopper = styled(Popper)({
  [`& .${autocompleteClasses.listbox}`]: {
    boxSizing: 'border-box',
    '& ul': {
      padding: 0,
      margin: 0,
    },
  },
});

const OPTIONS = Array.from(new Array(10000))
  .map(() => random(10 + Math.ceil(Math.random() * 20)))
  .sort((a: string, b: string) => a.toUpperCase().localeCompare(b.toUpperCase()));

export default function Virtualize() {
  // Use react-window v2's useListRef hook for imperative API access
  const internalListRef = useListRef(null);
  const optionIndexMapRef = React.useRef<Map<string, number>>(new Map());

  const handleItemsBuilt = React.useCallback(
    (optionIndexMap: Map<string, number>) => {
      optionIndexMapRef.current = optionIndexMap;
    },
    [],
  );

  // Handle keyboard navigation by scrolling to highlighted option
  const handleHighlightChange = (
    _event: React.SyntheticEvent,
    option: string | null,
  ) => {
    if (option && internalListRef.current) {
      const index = optionIndexMapRef.current.get(option);
      if (index !== undefined) {
        internalListRef.current.scrollToRow({ index, align: 'auto' });
      }
    }
  };

  return (
    <Autocomplete
      sx={{ width: 300 }}
      disableListWrap
      options={OPTIONS}
      renderInput={(params) => <TextField {...params} label="10,000 options" />}
      renderOption={(props, option, state) =>
        [props, option, state.index] as React.ReactNode
      }
      onHighlightChange={handleHighlightChange}
      slots={{
        popper: StyledPopper,
      }}
      slotProps={{
        listbox: {
          component: ListboxComponent,
          internalListRef,
          onItemsBuilt: handleItemsBuilt,
        } as any,
      }}
    />
  );
}

```

### `useAutocomplete`

Use the `useAutocomplete` hook when you need full control over markup. Import it from `@mui/material/useAutocomplete` ([4.6 kB gzipped](https://bundlephobia.com/package/@mui/material)); it accepts the same options as `Autocomplete`, minus the rendering props. The snippet shows the essential setup for a headless combobox.

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

interface Option {
  label: string;
}

interface MyAutocompleteProps {
  id: string;
  label: string;
  options: readonly Option[];
}

function MyAutocomplete({ id, label, options }: MyAutocompleteProps) {
  const {
    getRootProps,
    getInputLabelProps,
    getInputProps,
    getListboxProps,
    getOptionProps,
    groupedOptions,
  } = useAutocomplete({
    id,
    options,
  });

  return (
    <div>
      <div {...getRootProps()}>
        <label {...getInputLabelProps()}>{label}</label>
        <input {...getInputProps()} />
      </div>
      {groupedOptions.length > 0 ? (
        <ul {...getListboxProps()}>
          {groupedOptions.map((option, index) => {
            const { key, ...optionProps } = getOptionProps({ option, index });
            return (
              <li key={key} {...optionProps}>
                {option.label}
              </li>
            );
          })}
        </ul>
      ) : null}
    </div>
  );
}

export default function App() {
  return <MyAutocomplete id="movie" label="Movie" options={movies} />;
}

interface Film extends Option {
  year: number;
}

const movies: readonly Film[] = [
  { label: 'The Shawshank Redemption', year: 1994 },
  { label: 'The Godfather', year: 1972 },
];
```

This demo shows a fully customized multi-selection combobox built with `useAutocomplete`.

```tsx
import * as React from 'react';
import useAutocomplete, {
  type AutocompleteGetItemProps,
} from '@mui/material/useAutocomplete';
import { styled } from '@mui/material/styles';
import { autocompleteClasses } from '@mui/material/Autocomplete';

interface ProgrammingLanguage {
  id: string;
  value: string;
}

const langs: ProgrammingLanguage[] = [
  { id: 'js', value: 'JavaScript' },
  { id: 'ts', value: 'TypeScript' },
  { id: 'py', value: 'Python' },
  { id: 'java', value: 'Java' },
  { id: 'cpp', value: 'C++' },
  { id: 'cs', value: 'C#' },
  { id: 'php', value: 'PHP' },
  { id: 'ruby', value: 'Ruby' },
  { id: 'go', value: 'Go' },
  { id: 'rust', value: 'Rust' },
  { id: 'swift', value: 'Swift' },
];

const defaultLangs = [langs[0]];

export default function CustomizedHook() {
  const {
    getRootProps,
    getInputLabelProps,
    getInputProps,
    getItemProps,
    getListboxProps,
    getOptionProps,
    groupedOptions,
    value,
    focusedItem,
    popupOpen,
    setAnchorEl,
  } = useAutocomplete<ProgrammingLanguage, true, false, false>({
    id: 'customized-hook-demo',
    multiple: true,
    autoHighlight: true,
    defaultValue: defaultLangs,
    disableCloseOnSelect: true,
    options: langs,
    getOptionLabel: (option) => option.value,
    isOptionEqualToValue: (option, selectedValue) => option.id === selectedValue.id,
  });

  return (
    <Root {...getRootProps()}>
      <Label {...getInputLabelProps()}>Programming languages</Label>
      <InputWrapper
        ref={setAnchorEl}
        className={value.length > 0 ? 'hasChips' : undefined}
      >
        {value.map((option, index) => {
          const { key, ...itemProps } = getItemProps({ index });
          return (
            <StyledChip
              key={key}
              {...itemProps}
              label={option.value}
              highlighted={focusedItem === index}
            />
          );
        })}
        <input
          {...getInputProps()}
          placeholder={value.length > 0 ? '' : 'e.g. TypeScript'}
        />
      </InputWrapper>
      {popupOpen ? (
        <Listbox {...getListboxProps()}>
          {groupedOptions.length === 0 ? (
            <EmptyOption role="presentation">No languages found.</EmptyOption>
          ) : (
            groupedOptions.map((option, index) => {
              const { key, ...optionProps } = getOptionProps({ option, index });
              return (
                <Option key={key} {...optionProps}>
                  <CheckIcon />
                  <span>{option.value}</span>
                </Option>
              );
            })
          )}
        </Listbox>
      ) : null}
    </Root>
  );
}

const tokens = {
  light: {
    foreground: 'oklch(0.145 0 0)',
    foregroundRing: 'oklch(0.145 0 0 / 10%)',
    popover: 'oklch(1 0 0)',
    popoverForeground: 'oklch(0.145 0 0)',
    muted: 'oklch(0.97 0 0)',
    mutedForeground: 'oklch(0.556 0 0)',
    accent: 'oklch(0.97 0 0)',
    accentForeground: 'oklch(0.205 0 0)',
    input: 'oklch(0.922 0 0)',
    ring: 'oklch(0.708 0 0)',
    focusRing: 'oklch(0.708 0 0 / 50%)',
  },
  dark: {
    foreground: 'oklch(0.985 0 0)',
    foregroundRing: 'oklch(0.985 0 0 / 10%)',
    popover: 'oklch(0.205 0 0)',
    popoverForeground: 'oklch(0.985 0 0)',
    muted: 'oklch(0.269 0 0)',
    mutedForeground: 'oklch(0.708 0 0)',
    accent: 'oklch(0.269 0 0)',
    accentForeground: 'oklch(0.985 0 0)',
    input: 'oklch(1 0 0 / 15%)',
    ring: 'oklch(0.556 0 0)',
    focusRing: 'oklch(0.556 0 0 / 50%)',
  },
};

const Root = styled('div')(({ theme }) => ({
  width: '100%',
  maxWidth: 320,
  position: 'relative',
  display: 'flex',
  flexDirection: 'column',
  gap: 8,
  ...theme.applyStyles('dark', {
    colorScheme: 'dark',
  }),
}));

const Label = styled('label')(({ theme }) => ({
  display: 'flex',
  alignItems: 'center',
  gap: 8,
  fontSize: 14,
  lineHeight: 1,
  fontWeight: 500,
  color: tokens.light.foreground,
  userSelect: 'none',
  ...theme.applyStyles('dark', {
    color: tokens.dark.foreground,
  }),
}));

const InputWrapper = styled('div')(({ theme }) => ({
  boxSizing: 'border-box',
  display: 'flex',
  flexWrap: 'wrap',
  alignItems: 'center',
  gap: 4,
  width: '100%',
  minHeight: 32,
  paddingBlock: 4,
  paddingInline: 10,
  borderWidth: 1,
  borderStyle: 'solid',
  borderColor: tokens.light.input,
  borderRadius: 10,
  backgroundColor: 'transparent',
  backgroundClip: 'padding-box',
  color: tokens.light.foreground,
  cursor: 'text',
  fontSize: 14,
  transition: 'border-color 150ms, box-shadow 150ms, background-color 150ms',
  '&.hasChips': {
    paddingInline: 4,
  },
  '&:focus-within': {
    borderColor: tokens.light.ring,
    boxShadow: `0 0 0 3px ${tokens.light.focusRing}`,
  },
  '& input': {
    flex: 1,
    boxSizing: 'border-box',
    minWidth: 64,
    height: 24,
    margin: 0,
    padding: 0,
    border: 0,
    backgroundColor: 'transparent',
    color: tokens.light.foreground,
    fontFamily: 'inherit',
    fontSize: 14,
    fontWeight: 400,
    outline: 0,
  },
  '& input::placeholder': {
    color: tokens.light.mutedForeground,
  },
  ...theme.applyStyles('dark', {
    borderColor: tokens.dark.input,
    backgroundColor: 'oklch(1 0 0 / 4.5%)',
    '&:focus-within': {
      borderColor: tokens.dark.ring,
      boxShadow: `0 0 0 3px ${tokens.dark.focusRing}`,
    },
    '& input': {
      color: tokens.dark.foreground,
    },
    '& input::placeholder': {
      color: tokens.dark.mutedForeground,
    },
  }),
}));

interface ChipProps extends ReturnType<AutocompleteGetItemProps<true>> {
  label: string;
  className?: string;
  highlighted: boolean;
}

function Chip(props: ChipProps) {
  const { className, highlighted, label, onDelete, ...other } = props;

  return (
    <div
      {...other}
      className={[className, highlighted ? 'focused' : null]
        .filter(Boolean)
        .join(' ')}
      aria-label={label}
    >
      {label}
      <button
        type="button"
        tabIndex={-1}
        aria-label={`Remove ${label}`}
        onClick={onDelete}
      >
        <XIcon />
      </button>
    </div>
  );
}

const StyledChip = styled(Chip)<ChipProps>(({ theme }) => ({
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  gap: 4,
  width: 'fit-content',
  height: 21,
  overflow: 'hidden',
  paddingInline: 6,
  borderRadius: 6,
  backgroundColor: tokens.light.muted,
  color: tokens.light.foreground,
  fontSize: 12,
  fontWeight: 500,
  paddingRight: 0,
  whiteSpace: 'nowrap',
  outline: 0,
  cursor: 'default',
  userSelect: 'none',
  '&.focused, &:focus-within': {
    boxShadow: `0 0 0 2px ${tokens.light.focusRing}`,
  },
  '& button': {
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    width: 24,
    height: 24,
    marginLeft: -4,
    padding: 0,
    border: 0,
    borderRadius: 8,
    background: 'none',
    color: 'inherit',
    cursor: 'default',
    opacity: 0.5,
    transition: 'opacity 150ms, background-color 150ms',
  },
  '@media (hover: hover)': {
    '& button:hover': {
      backgroundColor: tokens.light.muted,
      opacity: 1,
    },
  },
  '& svg': {
    width: 16,
    height: 16,
    pointerEvents: 'none',
    flexShrink: 0,
  },
  ...theme.applyStyles('dark', {
    backgroundColor: tokens.dark.muted,
    color: tokens.dark.foreground,
    '&.focused, &:focus-within': {
      boxShadow: `0 0 0 2px ${tokens.dark.focusRing}`,
    },
    '@media (hover: hover)': {
      '& button:hover': {
        backgroundColor: 'oklch(0.269 0 0 / 50%)',
      },
    },
  }),
}));

const Listbox = styled('ul')(({ theme }) => ({
  boxSizing: 'border-box',
  position: 'absolute',
  top: 'calc(100% + 6px)',
  left: 0,
  zIndex: 50,
  width: '100%',
  maxWidth: '100vw',
  maxHeight: 252,
  margin: 0,
  padding: 4,
  overflowY: 'auto',
  overscrollBehavior: 'contain',
  scrollPaddingBlock: 4,
  borderRadius: 10,
  backgroundColor: tokens.light.popover,
  boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
  color: tokens.light.popoverForeground,
  listStyle: 'none',
  outline: `1px solid ${tokens.light.foregroundRing}`,
  transformOrigin: 'var(--transform-origin)',
  transitionDuration: '100ms',
  ...theme.applyStyles('dark', {
    backgroundColor: tokens.dark.popover,
    color: tokens.dark.popoverForeground,
    outlineColor: tokens.dark.foregroundRing,
  }),
}));

const Option = styled('li')(({ theme }) => ({
  boxSizing: 'border-box',
  position: 'relative',
  display: 'flex',
  width: '100%',
  alignItems: 'center',
  gap: 8,
  borderRadius: 8,
  paddingBlock: 4,
  paddingLeft: 6,
  paddingRight: 32,
  fontSize: 14,
  lineHeight: '20px',
  outline: 0,
  cursor: 'default',
  userSelect: 'none',
  '& svg': {
    position: 'absolute',
    right: 8,
    display: 'flex',
    width: 16,
    height: 16,
    alignItems: 'center',
    justifyContent: 'center',
    pointerEvents: 'none',
    flexShrink: 0,
    visibility: 'hidden',
  },
  "&[aria-selected='true']": {
    '& svg': {
      visibility: 'visible',
    },
  },
  [`&.${autocompleteClasses.focused}`]: {
    backgroundColor: tokens.light.accent,
    color: tokens.light.accentForeground,
  },
  ...theme.applyStyles('dark', {
    [`&.${autocompleteClasses.focused}`]: {
      backgroundColor: tokens.dark.accent,
      color: tokens.dark.accentForeground,
    },
  }),
}));

const EmptyOption = styled('li')(({ theme }) => ({
  boxSizing: 'border-box',
  display: 'flex',
  justifyContent: 'center',
  width: '100%',
  paddingBlock: 8,
  paddingInline: 0,
  color: tokens.light.mutedForeground,
  fontSize: 14,
  lineHeight: '20px',
  textAlign: 'center',
  ...theme.applyStyles('dark', {
    color: tokens.dark.mutedForeground,
  }),
}));

function CheckIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg fill="currentColor" width="10" height="10" viewBox="0 0 10 10" {...props}>
      <path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
    </svg>
  );
}

function XIcon(props: React.ComponentProps<'svg'>) {
  return (
    <svg
      width={16}
      height={16}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden
      {...props}
    >
      <path d="M18 6 6 18" />
      <path d="m6 6 12 12" />
    </svg>
  );
}

```

## Limitations

### autocomplete/autofill

Browsers use heuristics to try to autofill form inputs—but this can interfere with the Autocomplete experience.

By default, the component sets `autoComplete="off"` to disable the browser's autocomplete history (remembering past entries for a field). Google Chrome ignores this setting ([Issue 41239842](https://issues.chromium.org/issues/41239842)). A workaround: remove the `id` so the component generates a random one.

Browsers may also propose **autofill** suggestions (saved logins, addresses, payment details). To avoid these:

- Use a generic input id that doesn't hint at the field's purpose—`id="field1"` rather than `id="country"`. Leave it empty to get a random id.
- Set `autoComplete="new-password"` (some browsers will offer a strong password for these inputs):

  ```jsx
  <TextField
    {...params}
    slotProps={{
      ...params.slotProps,
      htmlInput: {
        ...params.slotProps.htmlInput,
        autoComplete: 'new-password',
      },
    }}
  />
  ```

Read [the guide on MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Turning_off_form_autocompletion) for more details.

### iOS VoiceOver

VoiceOver on iOS Safari has poor support for `aria-owns`. Work around it with the `disablePortal` prop.

### Custom listbox component

When you provide a custom `listbox` slot, set `role="listbox"` on the scroll container. This is what keyboard navigation looks for to scroll the highlighted item into view.


# Autocomplete API

## Demos

For examples and details on the usage of this React component, visit the component demo pages:

- [Autocomplete](https://deploy-preview-48470--material-ui.netlify.app/material-ui/react-autocomplete/)

## Import

```jsx
import Autocomplete from '@mui/material/Autocomplete';
// or
import { Autocomplete } from '@mui/material';
```

## Props

| Name | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| options | `array` | - | Yes |  |
| renderInput | `function(params: object) => ReactNode` | - | Yes |  |
| autoComplete | `bool` | `false` | No |  |
| autoHighlight | `bool` | `false` | No |  |
| autoSelect | `bool` | `false` | No |  |
| blurOnSelect | `'mouse' \| 'touch' \| bool` | `false` | No |  |
| classes | `object` | - | No | Override or extend the styles applied to the component. |
| clearIcon | `node` | `<ClearIcon fontSize="small" />` | No |  |
| clearOnBlur | `bool` | `!props.freeSolo` | No |  |
| clearOnEscape | `bool` | `false` | No |  |
| clearText | `string` | `'Clear'` | No |  |
| closeText | `string` | `'Close'` | No |  |
| defaultValue | `any` | `props.multiple ? [] : null` | No |  |
| disableClearable | `bool` | `false` | No |  |
| disableCloseOnSelect | `bool` | `false` | No |  |
| disabled | `bool` | `false` | No |  |
| disabledItemsFocusable | `bool` | `false` | No |  |
| disableListWrap | `bool` | `false` | No |  |
| disablePortal | `bool` | `false` | No |  |
| filterOptions | `function(options: Array<Value>, state: object) => Array<Value>` | `createFilterOptions()` | No |  |
| filterSelectedOptions | `bool` | `false` | No |  |
| forcePopupIcon | `'auto' \| bool` | `'auto'` | No |  |
| freeSolo | `bool` | `false` | No |  |
| fullWidth | `bool` | `false` | No |  |
| getLimitTagsText | `function(more: number) => ReactNode` | `(more) => `+${more}`` | No |  |
| getOptionDisabled | `function(option: Value) => boolean` | - | No |  |
| getOptionKey | `function(option: Value) => string \| number` | - | No |  |
| getOptionLabel | `function(option: Value \| string) => string` | `(option) => option.label ?? option` | No |  |
| groupBy | `function(option: Value) => string` | - | No |  |
| handleHomeEndKeys | `bool` | `!props.freeSolo` | No |  |
| id | `string` | - | No |  |
| includeInputInList | `bool` | `false` | No |  |
| inputValue | `string` | - | No |  |
| isOptionEqualToValue | `function(option: Value, value: Value \| string) => boolean` | - | No |  |
| limitTags | `integer` | `-1` | No |  |
| loading | `bool` | `false` | No |  |
| loadingText | `node` | `'Loading…'` | No |  |
| multiple | `bool` | `false` | No |  |
| noOptionsText | `node` | `'No options'` | No |  |
| onChange | `function(event: React.SyntheticEvent, value: Value \| Array<Value>, reason: string, details?: string) => void` | - | No |  |
| onClose | `function(event: React.SyntheticEvent, reason: string) => void` | - | No |  |
| onHighlightChange | `function(event: React.SyntheticEvent, option: Value, reason: string) => void` | - | No |  |
| onInputChange | `function(event: React.SyntheticEvent, value: string, reason: string) => void` | - | No |  |
| onOpen | `function(event: React.SyntheticEvent) => void` | - | No |  |
| open | `bool` | - | No |  |
| openOnFocus | `bool` | `false` | No |  |
| openText | `string` | `'Open'` | No |  |
| popupIcon | `node` | `<ArrowDropDownIcon />` | No |  |
| readOnly | `bool` | `false` | No |  |
| renderGroup | `function(params: AutocompleteRenderGroupParams) => ReactNode` | - | No |  |
| renderOption | `function(props: object, option: Value, state: object, ownerState: object) => ReactNode` | - | No |  |
| renderValue | `function(value: AutocompleteRenderValue<Value, Multiple, FreeSolo>, getItemProps: function, ownerState: object) => ReactNode` | - | No |  |
| resetHighlightOnMouseLeave (deprecated) | `bool` | `false` | No | ⚠️ This behavior will be enabled by default in the next major version. |
| selectOnFocus | `bool` | `!props.freeSolo` | No |  |
| size | `'small' \| 'medium' \| string` | `'medium'` | No |  |
| slotProps | `{ chip?: func \| object, clearIndicator?: func \| object, listbox?: func \| object, paper?: func \| object, popper?: func \| object, popupIndicator?: func \| object, root?: func \| object }` | `{}` | No |  |
| slots | `{ clearIndicator?: elementType, listbox?: elementType, paper?: elementType, popper?: elementType, popupIndicator?: elementType, root?: elementType }` | `{}` | No |  |
| sx | `Array<func \| object \| bool> \| func \| object` | - | No | The system prop that allows defining system overrides as well as additional CSS styles. |
| value | `any` | - | No |  |

> **Note**: The `ref` is forwarded to the root element.

> Any other props supplied will be provided to the root element (native element).

## Theme default props

You can use `MuiAutocomplete` to change the default props of this component with the theme.

## Slots

| Name | Default | Class | Description |
|------|---------|-------|-------------|
| root | `'div'` | `.MuiAutocomplete-root` | The component that renders the root. |
| clearIndicator | `IconButton` | `.MuiAutocomplete-clearIndicator` | The component used to render the clear indicator element. |
| popupIndicator | `IconButton` | `.MuiAutocomplete-popupIndicator` | The component used to render the popup indicator element. |
| listbox | `'ul'` | `.MuiAutocomplete-listbox` | The component used to render the listbox. |
| paper | `Paper` | `.MuiAutocomplete-paper` | The component used to render the body of the popup. |
| popper | `Popper` | `.MuiAutocomplete-popper` | The component used to position the popup. |

## CSS

### Rule name

| Global class | Rule name | Description |
|--------------|-----------|-------------|
| - | endAdornment | Styles applied to the endAdornment element. |
| `.Mui-expanded` | - | State class applied to the root element if the listbox is displayed. |
| `.Mui-focused` | - | State class applied to the root element if focused. |
| `.Mui-focusVisible` | - | Styles applied to the option elements if they are keyboard focused. |
| - | fullWidth | Styles applied to the root element if `fullWidth={true}`. |
| - | groupLabel | Styles applied to the group's label elements. |
| - | groupUl | Styles applied to the group's ul elements. |
| - | hasClearIcon | Styles applied when the clear icon is rendered. |
| - | hasPopupIcon | Styles applied when the popup icon is rendered. |
| - | input | Styles applied to the input element. |
| - | inputFocused | Styles applied to the input element if the input is focused. |
| - | inputRoot | Styles applied to the Input element. |
| - | loading | Styles applied to the loading wrapper. |
| - | noOptions | Styles applied to the no option wrapper. |
| - | option | Styles applied to the option elements. |
| - | popperDisablePortal | Styles applied to the popper element if `disablePortal={true}`. |
| - | popupIndicatorOpen | Styles applied to the popup indicator if the popup is open. |
| - | tag | Styles applied to the tag elements, for example the chips. |
| - | tagSizeMedium | Styles applied to the tag elements, for example the chips if `size="medium"`. |
| - | tagSizeSmall | Styles applied to the tag elements, for example the chips if `size="small"`. |

## Source code

If you did not find the information on this page, consider having a look at the implementation of the component for more detail.

- [/packages/mui-material/src/Autocomplete/Autocomplete.js](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/src/Autocomplete/Autocomplete.js)

# Popper API

## Demos

For examples and details on the usage of this React component, visit the component demo pages:

- [Autocomplete](https://deploy-preview-48470--material-ui.netlify.app/material-ui/react-autocomplete/)
- [Menu](https://deploy-preview-48470--material-ui.netlify.app/material-ui/react-menu/)
- [Popper](https://deploy-preview-48470--material-ui.netlify.app/material-ui/react-popper/)

## Import

```jsx
import Popper from '@mui/material/Popper';
// or
import { Popper } from '@mui/material';
```

## Props

| Name | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| open | `bool` | - | Yes |  |
| anchorEl | `HTML element \| object \| func` | - | No |  |
| children | `node \| func` | - | No |  |
| component | `elementType` | - | No |  |
| container | `HTML element \| func` | - | No |  |
| disablePortal | `bool` | `false` | No |  |
| keepMounted | `bool` | `false` | No |  |
| modifiers | `Array<{ data?: object, effect?: func, enabled?: bool, fn?: func, name?: any, options?: object, phase?: 'afterMain' \| 'afterRead' \| 'afterWrite' \| 'beforeMain' \| 'beforeRead' \| 'beforeWrite' \| 'main' \| 'read' \| 'write', requires?: Array<string>, requiresIfExists?: Array<string> }>` | - | No |  |
| placement | `'auto-end' \| 'auto-start' \| 'auto' \| 'bottom-end' \| 'bottom-start' \| 'bottom' \| 'left-end' \| 'left-start' \| 'left' \| 'right-end' \| 'right-start' \| 'right' \| 'top-end' \| 'top-start' \| 'top'` | `'bottom'` | No |  |
| popperOptions | `{ modifiers?: array, onFirstUpdate?: func, placement?: 'auto-end' \| 'auto-start' \| 'auto' \| 'bottom-end' \| 'bottom-start' \| 'bottom' \| 'left-end' \| 'left-start' \| 'left' \| 'right-end' \| 'right-start' \| 'right' \| 'top-end' \| 'top-start' \| 'top', strategy?: 'absolute' \| 'fixed' }` | `{}` | No |  |
| popperRef | `ref` | - | No |  |
| slotProps | `{ root?: func \| object }` | `{}` | No |  |
| slots | `{ root?: elementType }` | `{}` | No |  |
| sx | `Array<func \| object \| bool> \| func \| object` | - | No | The system prop that allows defining system overrides as well as additional CSS styles. |
| transition | `bool` | `false` | No |  |

> **Note**: The `ref` is forwarded to the root element (HTMLDivElement).

> Any other props supplied will be provided to the root element (native element).

## CSS

### Rule name

| Global class | Rule name | Description |
|--------------|-----------|-------------|
| - | root | Class name applied to the root element. |

## Source code

If you did not find the information on this page, consider having a look at the implementation of the component for more detail.

- [/packages/mui-material/src/Popper/Popper.tsx](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/src/Popper/Popper.tsx)

# TextField API

## Demos

For examples and details on the usage of this React component, visit the component demo pages:

- [Autocomplete](https://deploy-preview-48470--material-ui.netlify.app/material-ui/react-autocomplete/)
- [Text Field](https://deploy-preview-48470--material-ui.netlify.app/material-ui/react-text-field/)

## Import

```jsx
import TextField from '@mui/material/TextField';
// or
import { TextField } from '@mui/material';
```

## Props

| Name | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| autoComplete | `string` | - | No |  |
| autoFocus | `bool` | `false` | No |  |
| classes | `object` | - | No | Override or extend the styles applied to the component. |
| color | `'primary' \| 'secondary' \| 'error' \| 'info' \| 'success' \| 'warning' \| string` | `'primary'` | No |  |
| defaultValue | `any` | - | No |  |
| disabled | `bool` | `false` | No |  |
| error | `bool` | `false` | No |  |
| fullWidth | `bool` | `false` | No |  |
| helperText | `node` | - | No |  |
| id | `string` | - | No |  |
| inputRef | `ref` | - | No |  |
| label | `node` | - | No |  |
| margin | `'dense' \| 'none' \| 'normal'` | `'none'` | No |  |
| maxRows | `number \| string` | - | No |  |
| minRows | `number \| string` | - | No |  |
| multiline | `bool` | `false` | No |  |
| name | `string` | - | No |  |
| onChange | `function(event: object) => void` | - | No |  |
| placeholder | `string` | - | No |  |
| required | `bool` | `false` | No |  |
| rows | `number \| string` | - | No |  |
| select | `bool` | `false` | No |  |
| size | `'medium' \| 'small' \| string` | `'medium'` | No |  |
| slotProps | `{ formHelperText?: func \| object, htmlInput?: func \| object, input?: func \| object, inputLabel?: func \| object, select?: func \| object }` | `{}` | No |  |
| slots | `{ formHelperText?: elementType, htmlInput?: elementType, input?: elementType, inputLabel?: elementType, root?: elementType, select?: elementType }` | `{}` | No |  |
| sx | `Array<func \| object \| bool> \| func \| object` | - | No | The system prop that allows defining system overrides as well as additional CSS styles. |
| type | `string` | - | No |  |
| value | `any` | - | No |  |
| variant | `'filled' \| 'outlined' \| 'standard'` | `'outlined'` | No |  |

> **Note**: The `ref` is forwarded to the root element.

> Any other props supplied will be provided to the root element ([FormControl](https://deploy-preview-48470--material-ui.netlify.app/material-ui/api/form-control/)).

## Inheritance

While not explicitly documented above, the props of the [FormControl](https://deploy-preview-48470--material-ui.netlify.app/material-ui/api/form-control/) component are also available on TextField.

## Theme default props

You can use `MuiTextField` to change the default props of this component with the theme.

## Slots

| Name | Default | Class | Description |
|------|---------|-------|-------------|
| root | `FormControl` | `.MuiTextField-root` | The component that renders the root. |
| input | `OutlinedInput` | - | The component that renders the input. |
| inputLabel | `InputLabel` | - | The component that renders the input's label. |
| htmlInput | `'input'` | - | The html input element. |
| formHelperText | `FormHelperText` | - | The component that renders the helper text. |
| select | `Select` | - | The component that renders the select. |

## Source code

If you did not find the information on this page, consider having a look at the implementation of the component for more detail.

- [/packages/mui-material/src/TextField/TextField.js](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/src/TextField/TextField.js)