The autocomplete is a normal text input enhanced by a panel of suggested options.
The widget is useful for setting the value of a single-line textbox in one of two types of scenarios:
It’s meant to be an improved version of the “react-select” and “downshift” packages.
The value must be chosen from a predefined set of allowed values.
import * as React from 'react';
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" />}
/>
);
}
By default, the component accepts the following options structures:
interface AutocompleteOption {
label: string;
}
// or
type AutocompleteOption = string;
for instance:
const options = [
{ label: 'The Godfather', id: 1 },
{ label: 'Pulp Fiction', id: 2 },
];
// or
const options = ['The Godfather', 'Pulp Fiction'];
However, you can use different structures by providing a getOptionLabel
prop.
If your options are objects, you must provide the isOptionEqualToValue
prop to ensure correct selection and highlighting. By default, it uses strict equality to compare options with the current value.
Each of the following examples demonstrates one feature of the Autocomplete component.
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Stack from '@mui/material/Stack';
export default function Playground() {
const defaultProps = {
options: top100Films,
getOptionLabel: (option: FilmOptionType) => option.title,
};
const flatProps = {
options: top100Films.map((option) => option.title),
};
const [value, setValue] = React.useState<FilmOptionType | null>(null);
return (
<Stack spacing={1} sx={{ width: 300 }}>
<Autocomplete
{...defaultProps}
id="disable-close-on-select"
disableCloseOnSelect
renderInput={(params) => (
<TextField {...params} label="disableCloseOnSelect" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="clear-on-escape"
clearOnEscape
renderInput={(params) => (
<TextField {...params} label="clearOnEscape" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="disable-clearable"
disableClearable
renderInput={(params) => (
<TextField {...params} label="disableClearable" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="include-input-in-list"
includeInputInList
renderInput={(params) => (
<TextField {...params} label="includeInputInList" variant="standard" />
)}
/>
<Autocomplete
{...flatProps}
id="flat-demo"
renderInput={(params) => (
<TextField {...params} label="flat" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="controlled-demo"
value={value}
onChange={(event: any, newValue: FilmOptionType | null) => {
setValue(newValue);
}}
renderInput={(params) => (
<TextField {...params} label="controlled" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="auto-complete"
autoComplete
includeInputInList
renderInput={(params) => (
<TextField {...params} label="autoComplete" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="disable-list-wrap"
disableListWrap
renderInput={(params) => (
<TextField {...params} label="disableListWrap" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="open-on-focus"
openOnFocus
renderInput={(params) => (
<TextField {...params} label="openOnFocus" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="auto-highlight"
autoHighlight
renderInput={(params) => (
<TextField {...params} label="autoHighlight" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="auto-select"
autoSelect
renderInput={(params) => (
<TextField {...params} label="autoSelect" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="disabled"
disabled
renderInput={(params) => (
<TextField {...params} label="disabled" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="disable-portal"
disablePortal
renderInput={(params) => (
<TextField {...params} label="disablePortal" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="blur-on-select"
blurOnSelect
renderInput={(params) => (
<TextField {...params} label="blurOnSelect" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="clear-on-blur"
clearOnBlur
renderInput={(params) => (
<TextField {...params} label="clearOnBlur" variant="standard" />
)}
/>
<Autocomplete
{...defaultProps}
id="select-on-focus"
selectOnFocus
renderInput={(params) => (
<TextField {...params} label="selectOnFocus" variant="standard" />
)}
/>
<Autocomplete
{...flatProps}
id="readOnly"
readOnly
defaultValue={flatProps.options[13]}
renderInput={(params) => (
<TextField {...params} label="readOnly" variant="standard" />
)}
/>
</Stack>
);
}
interface FilmOptionType {
title: string;
year: number;
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
Choose one of the 248 countries.
import * as React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
export default function CountrySelect() {
return (
<Autocomplete
id="country-select-demo"
sx={{ width: 300 }}
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"
width="20"
srcSet={`https://flagcdn.com/w40/${option.code.toLowerCase()}.png 2x`}
src={`https://flagcdn.com/w20/${option.code.toLowerCase()}.png`}
alt=""
/>
{option.label} ({option.code}) +{option.phone}
</Box>
);
}}
renderInput={(params) => (
<TextField
{...params}
label="Choose a country"
slotProps={{
htmlInput: {
...params.inputProps,
autoComplete: 'new-password', // disable autocomplete and autofill
},
}}
/>
)}
/>
);
}
interface CountryType {
code: string;
label: string;
phone: string;
suggested?: boolean;
}
// From https://bitbucket.org/atlassian/atlaskit-mk-2/raw/4ad0e56649c3e6c973e226b7efaeb28cb240ccb0/packages/core/select/src/data/countries.js
const countries: readonly CountryType[] = [
{ code: 'AD', label: 'Andorra', phone: '376' },
... omitted for brevity ...
{ code: 'ZW', label: 'Zimbabwe', phone: '263' },
];
The component has two states that can be controlled:
value
/onChange
props combination. This state represents the value selected by the user, for instance when pressing Enter.inputValue
/onInputChange
props combination. This state represents the value displayed in the textbox.These two states are isolated, and should be controlled independently.
- A component is controlled when it’s managed by its parent using props.
- A component is uncontrolled when it’s managed by its own local state.
Learn more about controlled and uncontrolled components in the React documentation.
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
const options = ['Option 1', 'Option 2'];
export default function ControllableStates() {
const [value, setValue] = React.useState<string | null>(options[0]);
const [inputValue, setInputValue] = React.useState('');
return (
<div>
<div>{`value: ${value !== null ? `'${value}'` : 'null'}`}</div>
<div>{`inputValue: '${inputValue}'`}</div>
<br />
<Autocomplete
value={value}
onChange={(event: any, newValue: string | null) => {
setValue(newValue);
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
id="controllable-states-demo"
options={options}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Controllable" />}
/>
</div>
);
}
If you control the
value
, make sure it’s referentially stable between renders. In other words, the reference to the value shouldn’t change if the value itself doesn’t change.
// ⚠️ 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
is called and returns a new array every render. The fix includes memoizing the value, so it changes only when needed.
Set freeSolo
to true so the textbox can contain any arbitrary value.
The prop is designed to cover the primary use case of a search input with suggestions, for example Google search or react-autowhatever.
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import Autocomplete from '@mui/material/Autocomplete';
export default function FreeSolo() {
return (
<Stack spacing={2} sx={{ width: 300 }}>
<Autocomplete
id="free-solo-demo"
freeSolo
options={top100Films.map((option) => option.title)}
renderInput={(params) => <TextField {...params} label="freeSolo" />}
/>
<Autocomplete
freeSolo
id="free-solo-2-demo"
disableClearable
options={top100Films.map((option) => option.title)}
renderInput={(params) => (
<TextField
{...params}
label="Search input"
slotProps={{
input: {
...params.InputProps,
type: 'search',
},
}}
/>
)}
/>
</Stack>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
{ title: 'The Godfather', year: 1972 },
... omitted for brevity ...
{ title: '3 Idiots', year: 2009 },
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
Be careful when using the free solo mode with non-string options, as it may cause type mismatch.
The value created by typing into the textbox is always a string, regardless of the type of the options.
If you intend to use this mode for a combo box like experience (an enhanced version of a select element) we recommend setting:
selectOnFocus
to help the user clear the selected value.clearOnBlur
to help the user enter a new value.handleHomeEndKeys
to move focus inside the popup with the Home and End keys.Add "YOUR SEARCH"
.import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
const filter = createFilterOptions<FilmOptionType>();
export default function FreeSoloCreateOption() {
const [value, setValue] = React.useState<FilmOptionType | null>(null);
return (
<Autocomplete
value={value}
onChange={(event, newValue) => {
if (typeof newValue === 'string') {
setValue({
title: newValue,
});
} else if (newValue && newValue.inputValue) {
// Create a new value from the user input
setValue({
title: newValue.inputValue,
});
} else {
setValue(newValue);
}
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
const { inputValue } = params;
// Suggest the creation of a new value
const isExisting = options.some((option) => inputValue === option.title);
if (inputValue !== '' && !isExisting) {
filtered.push({
inputValue,
title: `Add "${inputValue}"`,
});
}
return filtered;
}}
selectOnFocus
clearOnBlur
handleHomeEndKeys
id="free-solo-with-text-demo"
options={top100Films}
getOptionLabel={(option) => {
// Value selected with enter, right from the input
if (typeof option === 'string') {
return option;
}
// Add "xxx" option created dynamically
if (option.inputValue) {
return option.inputValue;
}
// Regular option
return option.title;
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<li key={key} {...optionProps}>
{option.title}
</li>
);
}}
sx={{ width: 300 }}
freeSolo
renderInput={(params) => (
<TextField {...params} label="Free solo with text demo" />
)}
/>
);
}
interface FilmOptionType {
inputValue?: string;
title: string;
year?: number;
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films: readonly FilmOptionType[] = [
{ title: 'The Shawshank Redemption', year: 1994 },
{ title: 'The Godfather', year: 1972 },
... omitted for brevity ...
{ title: '3 Idiots', year: 2009 },
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
You could also display a dialog when the user wants to add a new value.
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 Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
const filter = createFilterOptions<FilmOptionType>();
export default function FreeSoloCreateOptionDialog() {
const [value, setValue] = React.useState<FilmOptionType | null>(null);
const [open, toggleOpen] = React.useState(false);
const handleClose = () => {
setDialogValue({
title: '',
year: '',
});
toggleOpen(false);
};
const [dialogValue, setDialogValue] = React.useState({
title: '',
year: '',
});
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setValue({
title: dialogValue.title,
year: parseInt(dialogValue.year, 10),
});
handleClose();
};
return (
<React.Fragment>
<Autocomplete
value={value}
onChange={(event, newValue) => {
if (typeof newValue === 'string') {
// timeout to avoid instant validation of the dialog's form.
setTimeout(() => {
toggleOpen(true);
setDialogValue({
title: newValue,
year: '',
});
});
} else if (newValue && newValue.inputValue) {
toggleOpen(true);
setDialogValue({
title: newValue.inputValue,
year: '',
});
} else {
setValue(newValue);
}
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
if (params.inputValue !== '') {
filtered.push({
inputValue: params.inputValue,
title: `Add "${params.inputValue}"`,
});
}
return filtered;
}}
id="free-solo-dialog-demo"
options={top100Films}
getOptionLabel={(option) => {
// for example value selected with enter, right from the input
if (typeof option === 'string') {
return option;
}
if (option.inputValue) {
return option.inputValue;
}
return option.title;
}}
selectOnFocus
clearOnBlur
handleHomeEndKeys
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<li key={key} {...optionProps}>
{option.title}
</li>
);
}}
sx={{ width: 300 }}
freeSolo
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>
<TextField
autoFocus
margin="dense"
id="name"
value={dialogValue.title}
onChange={(event) =>
setDialogValue({
...dialogValue,
title: event.target.value,
})
}
label="title"
type="text"
variant="standard"
/>
<TextField
margin="dense"
id="name"
value={dialogValue.year}
onChange={(event) =>
setDialogValue({
...dialogValue,
year: event.target.value,
})
}
label="year"
type="number"
variant="standard"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button type="submit">Add</Button>
</DialogActions>
</form>
</Dialog>
</React.Fragment>
);
}
interface FilmOptionType {
inputValue?: string;
title: string;
year?: number;
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films: readonly FilmOptionType[] = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
You can group the options with the groupBy
prop.
If you do so, make sure that the options are also sorted with the same dimension that they are grouped by,
otherwise, you will notice duplicate headers.
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
export default function Grouped() {
const options = top100Films.map((option) => {
const firstLetter = option.title[0].toUpperCase();
return {
firstLetter: /[0-9]/.test(firstLetter) ? '0-9' : firstLetter,
...option,
};
});
return (
<Autocomplete
options={options.sort((a, b) => -b.firstLetter.localeCompare(a.firstLetter))}
groupBy={(option) => option.firstLetter}
getOptionLabel={(option) => option.title}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="With categories" />}
/>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
To control how the groups are rendered, provide a custom renderGroup
prop.
This is a function that accepts an object with two fields:
group
—a string representing a group namechildren
—a collection of list items that belong to the groupThe following demo shows how to use this prop to define custom markup and override the styles of the default groups:
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import { styled, lighten, darken } from '@mui/system';
const GroupHeader = styled('div')(({ theme }) => ({
position: 'sticky',
top: '-8px',
padding: '4px 10px',
color: theme.palette.primary.main,
backgroundColor: lighten(theme.palette.primary.light, 0.85),
...theme.applyStyles('dark', {
backgroundColor: darken(theme.palette.primary.main, 0.8),
}),
}));
const GroupItems = styled('ul')({
padding: 0,
});
export default function RenderGroup() {
const options = top100Films.map((option) => {
const firstLetter = option.title[0].toUpperCase();
return {
firstLetter: /[0-9]/.test(firstLetter) ? '0-9' : firstLetter,
...option,
};
});
return (
<Autocomplete
options={options.sort((a, b) => -b.firstLetter.localeCompare(a.firstLetter))}
groupBy={(option) => option.firstLetter}
getOptionLabel={(option) => option.title}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="With categories" />}
renderGroup={(params) => (
<li key={params.key}>
<GroupHeader>{params.group}</GroupHeader>
<GroupItems>{params.children}</GroupItems>
</li>
)}
/>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
export default function DisabledOptions() {
return (
<Autocomplete
options={timeSlots}
getOptionDisabled={(option) =>
option === timeSlots[0] || option === timeSlots[2]
}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Disabled options" />}
/>
);
}
// One time slot every 30 minutes.
const timeSlots = Array.from(new Array(24 * 2)).map(
(_, index) =>
`${index < 20 ? '0' : ''}${Math.floor(index / 2)}:${
index % 2 === 0 ? '00' : '30'
}`,
);
useAutocomplete
For advanced customization use cases, a headless useAutocomplete()
hook is exposed.
It accepts almost the same options as the Autocomplete component minus all the props
related to the rendering of JSX.
The Autocomplete component is built on this hook.
import { useAutocomplete } from '@mui/base/useAutocomplete';
The useAutocomplete
hook is also reexported from @mui/material for convenience and backward compatibility.
import useAutocomplete from '@mui/material/useAutocomplete';
import * as React from 'react';
import { useAutocomplete } from '@mui/base/useAutocomplete';
import { styled } from '@mui/system';
const Label = styled('label')({
display: 'block',
});
const Input = styled('input')(({ theme }) => ({
width: 200,
backgroundColor: '#fff',
color: '#000',
...theme.applyStyles('dark', {
backgroundColor: '#000',
color: '#fff',
}),
}));
const Listbox = styled('ul')(({ theme }) => ({
width: 200,
margin: 0,
padding: 0,
zIndex: 1,
position: 'absolute',
listStyle: 'none',
backgroundColor: '#fff',
overflow: 'auto',
maxHeight: 200,
border: '1px solid rgba(0,0,0,.25)',
'& li.Mui-focused': {
backgroundColor: '#4a8df6',
color: 'white',
cursor: 'pointer',
},
'& li:active': {
backgroundColor: '#2977f5',
color: 'white',
},
...theme.applyStyles('dark', {
backgroundColor: '#000',
}),
}));
export default function UseAutocomplete() {
const {
getRootProps,
getInputLabelProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions,
} = useAutocomplete({
id: 'use-autocomplete-demo',
options: top100Films,
getOptionLabel: (option) => option.title,
});
return (
<div>
<div {...getRootProps()}>
<Label {...getInputLabelProps()}>useAutocomplete</Label>
<Input {...getInputProps()} />
</div>
{groupedOptions.length > 0 ? (
<Listbox {...getListboxProps()}>
{(groupedOptions as typeof top100Films).map((option, index) => {
const { key, ...optionProps } = getOptionProps({ option, index });
return (
<li key={key} {...optionProps}>
{option.title}
</li>
);
})}
</Listbox>
) : null}
</div>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
import * as React from 'react';
import { useAutocomplete, AutocompleteGetTagProps } from '@mui/base/useAutocomplete';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import { styled } from '@mui/material/styles';
import { autocompleteClasses } from '@mui/material/Autocomplete';
const Root = styled('div')(
({ theme }) => `
color: ${
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,.85)'
};
font-size: 14px;
`,
);
const Label = styled('label')`
padding: 0 0 4px;
line-height: 1.5;
display: block;
`;
const InputWrapper = styled('div')(
({ theme }) => `
width: 300px;
border: 1px solid ${theme.palette.mode === 'dark' ? '#434343' : '#d9d9d9'};
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'};
border-radius: 4px;
padding: 1px;
display: flex;
flex-wrap: wrap;
&:hover {
border-color: ${theme.palette.mode === 'dark' ? '#177ddc' : '#40a9ff'};
}
&.focused {
border-color: ${theme.palette.mode === 'dark' ? '#177ddc' : '#40a9ff'};
box-shadow: 0 0 0 2px rgb(24 144 255 / 0.2);
}
& input {
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'};
color: ${
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,.85)'
};
height: 30px;
box-sizing: border-box;
padding: 4px 6px;
width: 0;
min-width: 30px;
flex-grow: 1;
border: 0;
margin: 0;
outline: 0;
}
`,
);
interface TagProps extends ReturnType<AutocompleteGetTagProps> {
label: string;
}
function Tag(props: TagProps) {
const { label, onDelete, ...other } = props;
return (
<div {...other}>
<span>{label}</span>
<CloseIcon onClick={onDelete} />
</div>
);
}
const StyledTag = styled(Tag)<TagProps>(
({ theme }) => `
display: flex;
align-items: center;
height: 24px;
margin: 2px;
line-height: 22px;
background-color: ${
theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : '#fafafa'
};
border: 1px solid ${theme.palette.mode === 'dark' ? '#303030' : '#e8e8e8'};
border-radius: 2px;
box-sizing: content-box;
padding: 0 4px 0 10px;
outline: 0;
overflow: hidden;
&:focus {
border-color: ${theme.palette.mode === 'dark' ? '#177ddc' : '#40a9ff'};
background-color: ${theme.palette.mode === 'dark' ? '#003b57' : '#e6f7ff'};
}
& span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
& svg {
font-size: 12px;
cursor: pointer;
padding: 4px;
}
`,
);
const Listbox = styled('ul')(
({ theme }) => `
width: 300px;
margin: 2px 0 0;
padding: 0;
position: absolute;
list-style: none;
background-color: ${theme.palette.mode === 'dark' ? '#141414' : '#fff'};
overflow: auto;
max-height: 250px;
border-radius: 4px;
box-shadow: 0 2px 8px rgb(0 0 0 / 0.15);
z-index: 1;
& li {
padding: 5px 12px;
display: flex;
& span {
flex-grow: 1;
}
& svg {
color: transparent;
}
}
& li[aria-selected='true'] {
background-color: ${theme.palette.mode === 'dark' ? '#2b2b2b' : '#fafafa'};
font-weight: 600;
& svg {
color: #1890ff;
}
}
& li.${autocompleteClasses.focused} {
background-color: ${theme.palette.mode === 'dark' ? '#003b57' : '#e6f7ff'};
cursor: pointer;
& svg {
color: currentColor;
}
}
`,
);
export default function CustomizedHook() {
const {
getRootProps,
getInputLabelProps,
getInputProps,
getTagProps,
getListboxProps,
getOptionProps,
groupedOptions,
value,
focused,
setAnchorEl,
} = useAutocomplete({
id: 'customized-hook-demo',
defaultValue: [top100Films[1]],
multiple: true,
options: top100Films,
getOptionLabel: (option) => option.title,
});
return (
<Root>
<div {...getRootProps()}>
<Label {...getInputLabelProps()}>Customized hook</Label>
<InputWrapper ref={setAnchorEl} className={focused ? 'focused' : ''}>
{value.map((option: FilmOptionType, index: number) => {
const { key, ...tagProps } = getTagProps({ index });
return <StyledTag key={key} {...tagProps} label={option.title} />;
})}
<input {...getInputProps()} />
</InputWrapper>
</div>
{groupedOptions.length > 0 ? (
<Listbox {...getListboxProps()}>
{(groupedOptions as typeof top100Films).map((option, index) => {
const { key, ...optionProps } = getOptionProps({ option, index });
return (
<li key={key} {...optionProps}>
<span>{option.title}</span>
<CheckIcon fontSize="small" />
</li>
);
})}
</Listbox>
) : null}
</Root>
);
}
interface FilmOptionType {
title: string;
year: number;
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
Head to the customization section for an example with the Autocomplete
component instead of the hook.
The component supports two different asynchronous use-cases:
It displays a progress state as long as the network request is pending.
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
interface Film {
title: string;
year: number;
}
function sleep(duration: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
}
export default function Asynchronous() {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState<readonly Film[]>([]);
const [loading, setLoading] = React.useState(false);
const handleOpen = () => {
setOpen(true);
(async () => {
setLoading(true);
await sleep(1e3); // For demo purposes.
setLoading(false);
setOptions([...topFilms]);
})();
};
const handleClose = () => {
setOpen(false);
setOptions([]);
};
return (
<Autocomplete
sx={{ width: 300 }}
open={open}
onOpen={handleOpen}
onClose={handleClose}
isOptionEqualToValue={(option, value) => option.title === value.title}
getOptionLabel={(option) => option.title}
options={options}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label="Asynchronous"
slotProps={{
input: {
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
},
}}
/>
)}
/>
);
}
// Top films as rated by IMDb users. http://www.imdb.com/chart/top
const topFilms = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Interstellar', year: 2014 },
];
If your logic is fetching new options on each keystroke and using the current value of the textbox to filter on the server, you may want to consider throttling requests.
Additionally, you will need to disable the built-in filtering of the Autocomplete
component by
overriding the filterOptions
prop:
<Autocomplete filterOptions={(x) => x} />
A customized UI for Google Maps Places Autocomplete. For this demo, we need to load the Google Maps JavaScript and Google Places API.
The following demo relies on autosuggest-highlight, a small (1 kB) utility for highlighting text in autosuggest and autocomplete components.
import * as React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
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';
import { debounce } from '@mui/material/utils';
// 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';
function loadScript(src: string, position: HTMLElement | null, id: string) {
if (!position) {
return;
}
const script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('id', id);
script.src = src;
position.appendChild(script);
}
const autocompleteService = { current: null };
interface MainTextMatchedSubstrings {
offset: number;
length: number;
}
interface StructuredFormatting {
main_text: string;
secondary_text: string;
main_text_matched_substrings?: readonly MainTextMatchedSubstrings[];
}
interface PlaceType {
description: string;
structured_formatting: StructuredFormatting;
}
export default function GoogleMaps() {
const [value, setValue] = React.useState<PlaceType | null>(null);
const [inputValue, setInputValue] = React.useState('');
const [options, setOptions] = React.useState<readonly PlaceType[]>([]);
const loaded = React.useRef(false);
if (typeof window !== 'undefined' && !loaded.current) {
if (!document.querySelector('#google-maps')) {
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`,
document.querySelector('head'),
'google-maps',
);
}
loaded.current = true;
}
const fetch = React.useMemo(
() =>
debounce(
(
request: { input: string },
callback: (results?: readonly PlaceType[]) => void,
) => {
(autocompleteService.current as any).getPlacePredictions(
request,
callback,
);
},
400,
),
[],
);
React.useEffect(() => {
let active = true;
if (!autocompleteService.current && (window as any).google) {
autocompleteService.current = new (
window as any
).google.maps.places.AutocompleteService();
}
if (!autocompleteService.current) {
return undefined;
}
if (inputValue === '') {
setOptions(value ? [value] : []);
return undefined;
}
fetch({ input: inputValue }, (results?: readonly PlaceType[]) => {
if (active) {
let newOptions: readonly PlaceType[] = [];
if (value) {
newOptions = [value];
}
if (results) {
newOptions = [...newOptions, ...results];
}
setOptions(newOptions);
}
});
return () => {
active = false;
};
}, [value, inputValue, fetch]);
return (
<Autocomplete
sx={{ width: 300 }}
getOptionLabel={(option) =>
typeof option === 'string' ? option : option.description
}
filterOptions={(x) => x}
options={options}
autoComplete
includeInputInList
filterSelectedOptions
value={value}
noOptionsText="No locations"
onChange={(event: any, newValue: PlaceType | null) => {
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 item sx={{ display: 'flex', width: 44 }}>
<LocationOnIcon sx={{ color: 'text.secondary' }} />
</Grid>
<Grid item sx={{ width: 'calc(100% - 44px)', wordWrap: 'break-word' }}>
{parts.map((part, index) => (
<Box
key={index}
component="span"
sx={{ fontWeight: part.highlight ? 'bold' : 'regular' }}
>
{part.text}
</Box>
))}
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{option.structured_formatting.secondary_text}
</Typography>
</Grid>
</Grid>
</li>
);
}}
/>
);
}
:::error Before you can start using the Google Maps JavaScript API and Places API, you need to get your own API key. :::
Also known as tags, the user is allowed to enter more than one value.
import * as React from 'react';
import Chip from '@mui/material/Chip';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
export default function Tags() {
return (
<Stack spacing={3} sx={{ width: 500 }}>
<Autocomplete
multiple
id="tags-standard"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={[top100Films[13]]}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label="Multiple values"
placeholder="Favorites"
/>
)}
/>
<Autocomplete
multiple
id="tags-outlined"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={[top100Films[13]]}
filterSelectedOptions
renderInput={(params) => (
<TextField
{...params}
label="filterSelectedOptions"
placeholder="Favorites"
/>
)}
/>
<Autocomplete
multiple
id="tags-filled"
options={top100Films.map((option) => option.title)}
defaultValue={[top100Films[13].title]}
freeSolo
renderTags={(value: readonly string[], getTagProps) =>
value.map((option: string, index: number) => {
const { key, ...tagProps } = getTagProps({ index });
return (
<Chip variant="outlined" label={option} key={key} {...tagProps} />
);
})
}
renderInput={(params) => (
<TextField
{...params}
variant="filled"
label="freeSolo"
placeholder="Favorites"
/>
)}
/>
<Autocomplete
multiple
id="tags-readOnly"
options={top100Films.map((option) => option.title)}
defaultValue={[top100Films[12].title, top100Films[13].title]}
readOnly
renderInput={(params) => (
<TextField {...params} label="readOnly" placeholder="Favorites" />
)}
/>
</Stack>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
In the event that you need to lock certain tags so that they can’t be removed, you can set the chips disabled.
import * as React from 'react';
import Chip from '@mui/material/Chip';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
export default function FixedTags() {
const fixedOptions = [top100Films[6]];
const [value, setValue] = React.useState([...fixedOptions, top100Films[13]]);
return (
<Autocomplete
multiple
id="fixed-tags-demo"
value={value}
onChange={(event, newValue) => {
setValue([
...fixedOptions,
...newValue.filter((option) => !fixedOptions.includes(option)),
]);
}}
options={top100Films}
getOptionLabel={(option) => option.title}
renderTags={(tagValue, getTagProps) =>
tagValue.map((option, index) => {
const { key, ...tagProps } = getTagProps({ index });
return (
<Chip
key={key}
label={option.title}
{...tagProps}
disabled={fixedOptions.includes(option)}
/>
);
})
}
style={{ width: 500 }}
renderInput={(params) => (
<TextField {...params} label="Fixed tag" placeholder="Favorites" />
)}
/>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
import * as React from 'react';
import Checkbox from '@mui/material/Checkbox';
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';
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
export default function CheckboxesTags() {
return (
<Autocomplete
multiple
id="checkboxes-tags-demo"
options={top100Films}
disableCloseOnSelect
getOptionLabel={(option) => option.title}
renderOption={(props, option, { selected }) => {
const { key, ...optionProps } = props;
return (
<li key={key} {...optionProps}>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.title}
</li>
);
}}
style={{ width: 500 }}
renderInput={(params) => (
<TextField {...params} label="Checkboxes" placeholder="Favorites" />
)}
/>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Interstellar', year: 2014 },
];
You can use the limitTags
prop to limit the number of displayed options when not focused.
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
export default function LimitTags() {
return (
<Autocomplete
multiple
limitTags={2}
id="multiple-limit-tags"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={[top100Films[13], top100Films[12], top100Films[11]]}
renderInput={(params) => (
<TextField {...params} label="limitTags" placeholder="Favorites" />
)}
sx={{ width: '500px' }}
/>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
Fancy smaller inputs? Use the size
prop.
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Chip from '@mui/material/Chip';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
export default function Sizes() {
return (
<Stack spacing={2} sx={{ width: 500 }}>
<Autocomplete
id="size-small-standard"
size="small"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={top100Films[13]}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label="Size small"
placeholder="Favorites"
/>
)}
/>
<Autocomplete
multiple
id="size-small-standard-multi"
size="small"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={[top100Films[13]]}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label="Size small"
placeholder="Favorites"
/>
)}
/>
<Autocomplete
id="size-small-outlined"
size="small"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={top100Films[13]}
renderInput={(params) => (
<TextField {...params} label="Size small" placeholder="Favorites" />
)}
/>
<Autocomplete
multiple
id="size-small-outlined-multi"
size="small"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={[top100Films[13]]}
renderInput={(params) => (
<TextField {...params} label="Size small" placeholder="Favorites" />
)}
/>
<Autocomplete
id="size-small-filled"
size="small"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={top100Films[13]}
renderTags={(value, getTagProps) =>
value.map((option, index) => {
const { key, ...tagProps } = getTagProps({ index });
return (
<Chip
key={key}
variant="outlined"
label={option.title}
size="small"
{...tagProps}
/>
);
})
}
renderInput={(params) => (
<TextField
{...params}
variant="filled"
label="Size small"
placeholder="Favorites"
/>
)}
/>
<Autocomplete
multiple
id="size-small-filled-multi"
size="small"
options={top100Films}
getOptionLabel={(option) => option.title}
defaultValue={[top100Films[13]]}
renderTags={(value, getTagProps) =>
value.map((option, index) => {
const { key, ...tagProps } = getTagProps({ index });
return (
<Chip
key={key}
variant="outlined"
label={option.title}
size="small"
{...tagProps}
/>
);
})
}
renderInput={(params) => (
<TextField
{...params}
variant="filled"
label="Size small"
placeholder="Favorites"
/>
)}
/>
</Stack>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
The renderInput
prop allows you to customize the rendered input.
The first argument of this render prop contains props that you need to forward.
Pay specific attention to the ref
and inputProps
keys.
If you’re using a custom input component inside the Autocomplete, make sure that you forward the ref to the underlying DOM element.
import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
const options = ['Option 1', 'Option 2'];
export default function CustomInputAutocomplete() {
return (
<label>
Value:{' '}
<Autocomplete
sx={(theme) => ({
display: 'inline-block',
'& input': {
width: 200,
bgcolor: 'background.paper',
color: theme.palette.getContrastText(theme.palette.background.paper),
},
})}
id="custom-input-demo"
options={options}
renderInput={(params) => (
<div ref={params.InputProps.ref}>
<input type="text" {...params.inputProps} />
</div>
)}
/>
</label>
);
}
To globally customize the Autocomplete options for all components in your app,
you can use the theme default props and set the renderOption
property in the defaultProps
key.
The renderOption
property takes the ownerState
as the fourth parameter, which includes props and internal component state.
To display the label, you can use the getOptionLabel
prop from the ownerState
.
This approach enables different options for each Autocomplete component while keeping the options styling consistent.
import * as React from 'react';
import Autocomplete, { autocompleteClasses } from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import { createTheme, useTheme, ThemeProvider, Theme } from '@mui/material/styles';
// Theme.ts
const customTheme = (outerTheme: Theme) =>
createTheme({
palette: {
mode: outerTheme.palette.mode,
},
components: {
MuiAutocomplete: {
defaultProps: {
renderOption: (props, option, state, ownerState) => {
const { key, ...optionProps } = props;
return (
<Box
key={key}
sx={{
borderRadius: '8px',
margin: '5px',
[`&.${autocompleteClasses.option}`]: {
padding: '8px',
},
}}
component="li"
{...optionProps}
>
{ownerState.getOptionLabel(option)}
</Box>
);
},
},
},
},
});
export default function GloballyCustomizedOptions() {
// useTheme is used to determine the dark or light mode of the docs to maintain the Autocomplete component default styles.
const outerTheme = useTheme();
return (
<ThemeProvider theme={customTheme(outerTheme)}>
<Stack spacing={5} sx={{ width: 300 }}>
<MovieSelect />
<CountrySelect />
</Stack>
</ThemeProvider>
);
}
function MovieSelect() {
return (
<Autocomplete
options={top100Films}
getOptionLabel={(option: FilmOptionType) => `${option.title} (${option.year})`}
id="movie-customized-option-demo"
disableCloseOnSelect
renderInput={(params) => (
<TextField {...params} label="Choose a movie" variant="standard" />
)}
/>
);
}
function CountrySelect() {
return (
<Autocomplete
options={countries}
disableCloseOnSelect
getOptionLabel={(option: CountryType) =>
`${option.label} (${option.code}) +${option.phone}`
}
renderInput={(params) => <TextField {...params} label="Choose a country" />}
/>
);
}
interface CountryType {
code: string;
label: string;
phone: string;
suggested?: boolean;
}
// From https://bitbucket.org/atlassian/atlaskit-mk-2/raw/4ad0e56649c3e6c973e226b7efaeb28cb240ccb0/packages/core/select/src/data/countries.js
const countries: readonly CountryType[] = [
{ code: 'AD', label: 'Andorra', phone: '376' },
... omitted for brevity ...
{ code: 'ZW', label: 'Zimbabwe', phone: '263' },
];
interface FilmOptionType {
title: string;
year: number;
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
This demo reproduces GitHub’s label picker:
import * as React from 'react';
import { useTheme, styled } from '@mui/material/styles';
import Popper from '@mui/material/Popper';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import SettingsIcon from '@mui/icons-material/Settings';
import CloseIcon from '@mui/icons-material/Close';
import DoneIcon from '@mui/icons-material/Done';
import Autocomplete, {
AutocompleteCloseReason,
autocompleteClasses,
} from '@mui/material/Autocomplete';
import ButtonBase from '@mui/material/ButtonBase';
import InputBase from '@mui/material/InputBase';
import Box from '@mui/material/Box';
interface PopperComponentProps {
anchorEl?: any;
disablePortal?: boolean;
open: boolean;
}
const StyledAutocompletePopper = styled('div')(({ theme }) => ({
[`& .${autocompleteClasses.paper}`]: {
boxShadow: 'none',
margin: 0,
color: 'inherit',
fontSize: 13,
},
[`& .${autocompleteClasses.listbox}`]: {
backgroundColor: '#fff',
padding: 0,
[`& .${autocompleteClasses.option}`]: {
minHeight: 'auto',
alignItems: 'flex-start',
padding: 8,
borderBottom: `1px solid ${' #eaecef'}`,
'&[aria-selected="true"]': {
backgroundColor: 'transparent',
},
[`&.${autocompleteClasses.focused}, &.${autocompleteClasses.focused}[aria-selected="true"]`]:
{
backgroundColor: theme.palette.action.hover,
},
...theme.applyStyles('dark', {
borderBottom: `1px solid ${'#30363d'}`,
}),
},
...theme.applyStyles('dark', {
backgroundColor: '#1c2128',
}),
},
[`&.${autocompleteClasses.popperDisablePortal}`]: {
position: 'relative',
},
}));
function PopperComponent(props: PopperComponentProps) {
const { disablePortal, anchorEl, open, ...other } = props;
return <StyledAutocompletePopper {...other} />;
}
const StyledPopper = styled(Popper)(({ theme }) => ({
border: `1px solid ${'#e1e4e8'}`,
boxShadow: `0 8px 24px ${'rgba(149, 157, 165, 0.2)'}`,
color: '#24292e',
backgroundColor: '#fff',
borderRadius: 6,
width: 300,
zIndex: theme.zIndex.modal,
fontSize: 13,
...theme.applyStyles('dark', {
border: `1px solid ${'#30363d'}`,
boxShadow: `0 8px 24px ${'rgb(1, 4, 9)'}`,
color: '#c9d1d9',
backgroundColor: '#1c2128',
}),
}));
const StyledInput = styled(InputBase)(({ theme }) => ({
padding: 10,
width: '100%',
borderBottom: `1px solid ${'#30363d'}`,
'& input': {
borderRadius: 4,
backgroundColor: '#fff',
border: `1px solid ${'#30363d'}`,
padding: 8,
transition: theme.transitions.create(['border-color', 'box-shadow']),
fontSize: 14,
'&:focus': {
boxShadow: `0px 0px 0px 3px ${'rgba(3, 102, 214, 0.3)'}`,
borderColor: '#0366d6',
...theme.applyStyles('dark', {
boxShadow: `0px 0px 0px 3px ${'rgb(12, 45, 107)'}`,
borderColor: '#388bfd',
}),
},
...theme.applyStyles('dark', {
backgroundColor: '#0d1117',
border: `1px solid ${'#eaecef'}`,
}),
},
...theme.applyStyles('dark', {
borderBottom: `1px solid ${'#eaecef'}`,
}),
}));
const Button = styled(ButtonBase)(({ theme }) => ({
fontSize: 13,
width: '100%',
textAlign: 'left',
paddingBottom: 8,
color: '#586069',
fontWeight: 600,
'&:hover,&:focus': {
color: '#0366d6',
...theme.applyStyles('dark', {
color: '#58a6ff',
}),
},
'& span': {
width: '100%',
},
'& svg': {
width: 16,
height: 16,
},
...theme.applyStyles('dark', {
color: '#8b949e',
}),
}));
export default function GitHubLabel() {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [value, setValue] = React.useState<LabelType[]>([labels[1], labels[11]]);
const [pendingValue, setPendingValue] = React.useState<LabelType[]>([]);
const theme = useTheme();
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setPendingValue(value);
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setValue(pendingValue);
if (anchorEl) {
anchorEl.focus();
}
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'github-label' : undefined;
return (
<React.Fragment>
<Box sx={{ width: 221, fontSize: 13 }}>
<Button disableRipple aria-describedby={id} onClick={handleClick}>
<span>Labels</span>
<SettingsIcon />
</Button>
{value.map((label) => (
<Box
key={label.name}
sx={{
mt: '3px',
height: 20,
padding: '.15em 4px',
fontWeight: 600,
lineHeight: '15px',
borderRadius: '2px',
}}
style={{
backgroundColor: label.color,
color: theme.palette.getContrastText(label.color),
}}
>
{label.name}
</Box>
))}
</Box>
<StyledPopper id={id} open={open} anchorEl={anchorEl} placement="bottom-start">
<ClickAwayListener onClickAway={handleClose}>
<div>
<Box
sx={(t) => ({
borderBottom: `1px solid ${'#30363d'}`,
padding: '8px 10px',
fontWeight: 600,
...t.applyStyles('light', {
borderBottom: `1px solid ${'#eaecef'}`,
}),
})}
>
Apply labels to this pull request
</Box>
<Autocomplete
open
multiple
onClose={(
event: React.ChangeEvent<{}>,
reason: AutocompleteCloseReason,
) => {
if (reason === 'escape') {
handleClose();
}
}}
value={pendingValue}
onChange={(event, newValue, reason) => {
if (
event.type === 'keydown' &&
((event as React.KeyboardEvent).key === 'Backspace' ||
(event as React.KeyboardEvent).key === 'Delete') &&
reason === 'removeOption'
) {
return;
}
setPendingValue(newValue);
}}
disableCloseOnSelect
renderTags={() => null}
noOptionsText="No labels"
renderOption={(props, option, { selected }) => {
const { key, ...optionProps } = props;
return (
<li key={key} {...optionProps}>
<Box
component={DoneIcon}
sx={{ width: 17, height: 17, mr: '5px', ml: '-2px' }}
style={{
visibility: selected ? 'visible' : 'hidden',
}}
/>
<Box
component="span"
sx={{
width: 14,
height: 14,
flexShrink: 0,
borderRadius: '3px',
mr: 1,
mt: '2px',
}}
style={{ backgroundColor: option.color }}
/>
<Box
sx={(t) => ({
flexGrow: 1,
'& span': {
color: '#8b949e',
...t.applyStyles('light', {
color: '#586069',
}),
},
})}
>
{option.name}
<br />
<span>{option.description}</span>
</Box>
<Box
component={CloseIcon}
sx={{ opacity: 0.6, width: 18, height: 18 }}
style={{
visibility: selected ? 'visible' : 'hidden',
}}
/>
</li>
);
}}
options={[...labels].sort((a, b) => {
// Display the selected labels first.
let ai = value.indexOf(a);
ai = ai === -1 ? value.length + labels.indexOf(a) : ai;
let bi = value.indexOf(b);
bi = bi === -1 ? value.length + labels.indexOf(b) : bi;
return ai - bi;
})}
getOptionLabel={(option) => option.name}
renderInput={(params) => (
<StyledInput
ref={params.InputProps.ref}
inputProps={params.inputProps}
autoFocus
placeholder="Filter labels"
/>
)}
slots={{
popper: PopperComponent,
}}
/>
</div>
</ClickAwayListener>
</StyledPopper>
</React.Fragment>
);
}
interface LabelType {
name: string;
color: string;
description?: string;
}
// From https://github.com/abdonrd/github-labels
const labels = [
{
name: 'good first issue',
color: '#7057ff',
description: 'Good for newcomers',
},
{
name: 'help wanted',
color: '#008672',
description: 'Extra attention is needed',
},
{
name: 'priority: critical',
color: '#b60205',
description: '',
},
... omitted for brevity ...
{
name: 'type: epic',
color: '#3e4b9e',
description: 'A theme of work that contain sub-tasks',
},
{
name: 'type: feature request',
color: '#fbca04',
description: 'New feature or request',
},
{
name: 'type: question',
color: '#d876e3',
description: 'Further information is requested',
},
];
Head to the Customized hook section for a customization example with the useAutocomplete
hook instead of the component.
The following demo shows how to add a hint feature to the Autocomplete:
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';
export default function AutocompleteHint() {
const hint = React.useRef('');
const [inputValue, setInputValue] = React.useState('');
return (
<Autocomplete
onKeyDown={(event) => {
if (event.key === 'Tab') {
if (hint.current) {
setInputValue(hint.current);
event.preventDefault();
}
}
}}
onClose={() => {
hint.current = '';
}}
onChange={(event, newValue) => {
setInputValue(newValue && newValue.label ? newValue.label : '');
}}
disablePortal
inputValue={inputValue}
id="combo-box-hint-demo"
options={top100Films}
sx={{ width: 300 }}
renderInput={(params) => {
return (
<Box sx={{ position: 'relative' }}>
<Typography
sx={{
position: 'absolute',
opacity: 0.5,
left: 14,
top: 16,
overflow: 'hidden',
whiteSpace: 'nowrap',
width: 'calc(100% - 75px)', // Adjust based on padding of TextField
}}
>
{hint.current}
</Typography>
<TextField
{...params}
onChange={(event) => {
const newValue = event.target.value;
setInputValue(newValue);
const matchingOption = top100Films.find((option) =>
option.label.startsWith(newValue),
);
if (newValue && matchingOption) {
hint.current = matchingOption.label;
} else {
hint.current = '';
}
}}
label="Movie"
/>
</Box>
);
}}
/>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ label: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ label: 'Monty Python and the Holy Grail', year: 1975 },
];
The following demo relies on autosuggest-highlight, a small (1 kB) utility for highlighting text in autosuggest and autocomplete components.
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import parse from 'autosuggest-highlight/parse';
import match from 'autosuggest-highlight/match';
export default function Highlights() {
return (
<Autocomplete
sx={{ width: 300 }}
options={top100Films}
getOptionLabel={(option) => option.title}
renderInput={(params) => (
<TextField {...params} label="Highlights" margin="normal" />
)}
renderOption={(props, option, { inputValue }) => {
const { key, ...optionProps } = props;
const matches = match(option.title, inputValue, { insideWords: true });
const parts = parse(option.title, matches);
return (
<li key={key} {...optionProps}>
<div>
{parts.map((part, index) => (
<span
key={index}
style={{
fontWeight: part.highlight ? 700 : 400,
}}
>
{part.text}
</span>
))}
</div>
</li>
);
}}
/>
);
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
The component exposes a factory to create a filter method that can be provided to the filterOptions
prop.
You can use it to change the default option filter behavior.
import { createFilterOptions } from '@mui/material/Autocomplete';
createFilterOptions(config) => filterOptions
config
(object [optional]):config.ignoreAccents
(bool [optional]): Defaults to true
. Remove diacritics.config.ignoreCase
(bool [optional]): Defaults to true
. Lowercase everything.config.limit
(number [optional]): Default to null. Limit the number of suggested options to be shown. For example, if config.limit
is 100
, only the first 100
matching options are shown. It can be useful if a lot of options match and virtualization wasn’t set up.config.matchFrom
(‘any’ | ‘start’ [optional]): Defaults to 'any'
.config.stringify
(func [optional]): Controls how an option is converted into a string so that it can be matched against the input text fragment.config.trim
(bool [optional]): Defaults to false
. Remove trailing spaces.filterOptions
: the returned filter method can be provided directly to the filterOptions
prop of the Autocomplete
component, or the parameter of the same name for the hook.
In the following demo, the options need to start with the query prefix:
const filterOptions = createFilterOptions({
matchFrom: 'start',
stringify: (option) => option.title,
});
<Autocomplete filterOptions={filterOptions} />;
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
const filterOptions = createFilterOptions({
matchFrom: 'start',
stringify: (option: FilmOptionType) => option.title,
});
export default function Filter() {
return (
<Autocomplete
options={top100Films}
getOptionLabel={(option) => option.title}
filterOptions={filterOptions}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Custom filter" />}
/>
);
}
interface FilmOptionType {
title: string;
year: number;
}
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
... omitted for brevity ...
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
For richer filtering mechanisms, like fuzzy matching, it’s recommended to look at match-sorter. For instance:
import { matchSorter } from 'match-sorter';
const filterOptions = (options, { inputValue }) => matchSorter(options, inputValue);
<Autocomplete filterOptions={filterOptions} />;
Search within 10,000 randomly generated options. The list is virtualized thanks to react-window.
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 ListSubheader from '@mui/material/ListSubheader';
import Popper from '@mui/material/Popper';
import { useTheme, styled } from '@mui/material/styles';
import { VariableSizeList, ListChildComponentProps } from 'react-window';
import Typography from '@mui/material/Typography';
const LISTBOX_PADDING = 8; // px
function renderRow(props: ListChildComponentProps) {
const { data, index, style } = props;
const dataSet = data[index];
const inlineStyle = {
...style,
top: (style.top as number) + LISTBOX_PADDING,
};
if (dataSet.hasOwnProperty('group')) {
return (
<ListSubheader key={dataSet.key} component="div" style={inlineStyle}>
{dataSet.group}
</ListSubheader>
);
}
const { key, ...optionProps } = dataSet[0];
return (
<Typography key={key} component="li" {...optionProps} noWrap style={inlineStyle}>
{`#${dataSet[2] + 1} - ${dataSet[1]}`}
</Typography>
);
}
const OuterElementContext = React.createContext({});
const OuterElementType = React.forwardRef<HTMLDivElement>((props, ref) => {
const outerProps = React.useContext(OuterElementContext);
return <div ref={ref} {...props} {...outerProps} />;
});
function useResetCache(data: any) {
const ref = React.useRef<VariableSizeList>(null);
React.useEffect(() => {
if (ref.current != null) {
ref.current.resetAfterIndex(0, true);
}
}, [data]);
return ref;
}
// Adapter for react-window
const ListboxComponent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLElement>
>(function ListboxComponent(props, ref) {
const { children, ...other } = props;
const itemData: React.ReactElement<unknown>[] = [];
(children as React.ReactElement<unknown>[]).forEach(
(
item: React.ReactElement<unknown> & {
children?: React.ReactElement<unknown>[];
},
) => {
itemData.push(item);
itemData.push(...(item.children || []));
},
);
const theme = useTheme();
const smUp = useMediaQuery(theme.breakpoints.up('sm'), {
noSsr: true,
});
const itemCount = itemData.length;
const itemSize = smUp ? 36 : 48;
const getChildSize = (child: React.ReactElement<unknown>) => {
if (child.hasOwnProperty('group')) {
return 48;
}
return itemSize;
};
const getHeight = () => {
if (itemCount > 8) {
return 8 * itemSize;
}
return itemData.map(getChildSize).reduce((a, b) => a + b, 0);
};
const gridRef = useResetCache(itemCount);
return (
<div ref={ref}>
<OuterElementContext.Provider value={other}>
<VariableSizeList
itemData={itemData}
height={getHeight() + 2 * LISTBOX_PADDING}
width="100%"
ref={gridRef}
outerElementType={OuterElementType}
innerElementType="ul"
itemSize={(index) => getChildSize(itemData[index])}
overscanCount={5}
itemCount={itemCount}
>
{renderRow}
</VariableSizeList>
</OuterElementContext.Provider>
</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() {
return (
<Autocomplete
sx={{ width: 300 }}
disableListWrap
options={OPTIONS}
groupBy={(option) => option[0].toUpperCase()}
renderInput={(params) => <TextField {...params} label="10,000 options" />}
renderOption={(props, option, state) =>
[props, option, state.index] as React.ReactNode
}
renderGroup={(params) => params as any}
slots={{
popper: StyledPopper,
}}
slotProps={{
listbox: {
component: ListboxComponent,
},
}}
/>
);
}
If you would like to prevent the default key handler behavior, you can set the event’s defaultMuiPrevented
property to true
:
<Autocomplete
onKeyDown={(event) => {
if (event.key === 'Enter') {
// Prevent's default 'Enter' behavior.
event.defaultMuiPrevented = true;
// your handler code
}
}}
/>
Browsers have heuristics to help the user fill in form inputs. However, this can harm the UX of the component.
By default, the component disables the input autocomplete feature (remembering what the user has typed for a given field in a previous session) with the autoComplete="off"
attribute.
Google Chrome does not currently support this attribute setting (Issue 41239842).
A possible workaround is to remove the id
to have the component generate a random one.
In addition to remembering past entered values, the browser might also propose autofill suggestions (saved login, address, or payment details). In the event you want the avoid autofill, you can try the following:
Name the input without leaking any information the browser can use. For example id="field1"
instead of id="country"
. If you leave the id empty, the component uses a random id.
Set autoComplete="new-password"
(some browsers will suggest a strong password for inputs with this attribute setting):
<TextField
{...params}
inputProps={{
...params.inputProps,
autoComplete: 'new-password',
}}
/>
Read the guide on MDN for more details.
VoiceOver on iOS Safari doesn’t support the aria-owns
attribute very well.
You can work around the issue with the disablePortal
prop.
If you provide a custom ListboxComponent
prop, you need to make sure that the intended scroll container has the role
attribute set to listbox
. This ensures the correct behavior of the scroll, for example when using the keyboard to navigate.
(WAI-ARIA: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/)
We encourage the usage of a label for the textbox. The component implements the WAI-ARIA authoring practices.