Skip to main content
react-md

Autocomplete

An Autocomplete is a component that allows for real-time suggestions from a pre-determined list as the user types by filtering data based on the current value. It can also be used to interact with an API that handles the sorting, filtering, matching, etc as well.

Simple Example

An Autocomplete requires a list of options which can be strings or an object with a label or name string so that the options can be filtered as the user types. In addition, a listboxLabel or listboxLabelledby prop must be defined for accessibility to provide a label for the listbox.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function SimpleExample(): ReactElement {
  return (
    <Autocomplete
      label="Fruit"
      placeholder="Apple"
      listboxLabel="Fruits"
      options={fruits}
    />
  );
}

export const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blueberry",
  "Cranberry",
  "Kiwi",
  "Mango",
  "Orange",
  "Peach",
  "Plum",
  "Strawberry",
];

Press Enter to start editing.

Simple Label Example

This is the same example as above, but using options as { label: string }.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function SimpleLabelExample(): ReactElement {
  return (
    <Autocomplete
      label="Fruit"
      placeholder="Apple"
      listboxLabel="Fruits"
      options={options}
    />
  );
}

const options = [
  { label: "Apple" },
  { label: "Apricot" },
  { label: "Banana" },
  { label: "Blueberry" },
  { label: "Cranberry" },
  { label: "Kiwi" },
  { label: "Mango" },
  { label: "Orange" },
  { label: "Peach" },
  { label: "Plum" },
  { label: "Strawberry" },
];

Press Enter to start editing.

Object Options

The Autocomplete can support any object option by also providing an getOptionLabel prop that will return a string value for each option.

See Typescript Typing for type behavior.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function ObjectOptionsExample(): ReactElement {
  return (
    <Autocomplete label="State" options={desserts} listboxLabel="States" />
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Option Props

The Autocomplete will automatically pass through common Option props that exist on each option.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { cssUtils } from "@react-md/core/cssUtils";
import FavoriteIcon from "@react-md/material-icons/FavoriteIcon";
import { type ReactElement } from "react";

const options = [
  {
    label: "Favorite Left Addon",
    leftAddon: <FavoriteIcon />,
  },
  {
    label: "Favorite Right Addon",
    // children will be shown in the Option by default and should normally
    // contain the same text as the `label` since the label is the searchable
    // part. the main usage of the `children` is to apply any custom styles or
    // highlighting
    children: (
      <>
        <span className={cssUtils({ fontWeight: "bold" })}>Favorite</span>{" "}
        <span className={cssUtils({ textDecoration: "underline" })}>Right</span>{" "}
        Addon
      </>
    ),
    rightAddon: <FavoriteIcon />,
  },
  {
    label: "Multiline",
    secondaryText: "Second line of text that is ignored in filtering",
    multiline: true,
  },
];

export default function OptionPropsExample(): ReactElement {
  return (
    <Autocomplete label="Label" options={options} listboxLabel="Options" />
  );
}

Press Enter to start editing.

Get Option Props

Another way to pass props to each option is using the getOptionProps function.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { Avatar } from "@react-md/core/avatar/Avatar";
import { cssUtils } from "@react-md/core/cssUtils";
import { cnb } from "cnbuilder";
import { type ReactElement } from "react";

export default function GetOptionPropsExample(): ReactElement {
  return (
    <Autocomplete
      label="Dessert"
      options={desserts}
      getOptionProps={({ index, option }) => ({
        className: cnb(
          index % 3 === 0 && cssUtils({ textDecoration: "line-through" }),
        ),
        leftAddon: <Avatar size="icon">{option.type.charAt(0)}</Avatar>,
      })}
      listboxLabel="Desserts"
    />
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Disabled Options

An option can be disabled by adding disabled: true to the option or using getOptionProps to return disabled: true.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function DisabledOptionsExample(): ReactElement {
  return (
    <Autocomplete
      label="Dessert"
      options={desserts.map((dessert, i) => ({
        ...dessert,
        disabled: i % 4 === 0,
      }))}
      // or try
      // getOptionProps={({ index, option }) => ({
      //   disabled: option.disabled || index % 4 === 0 || option.type === "Other",
      // })}
      listboxLabel="Desserts"
    />
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Controlling the Value

If the current value is needed, provide a value and setValue prop.

Value: "null"

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { Box } from "@react-md/core/box/Box";
import { Typography } from "@react-md/core/typography/Typography";
import { type ReactElement, useState } from "react";

export default function ControllingTheValueExample(): ReactElement {
  const [value, setValue] = useState<State | null>(null);

  return (
    <Box align="start" stacked>
      <Autocomplete
        label="State"
        value={value}
        setValue={setValue}
        options={states}
        listboxLabel="States"
      />
      <Typography>Value: {`"${value?.name || null}"`}</Typography>
    </Box>
  );
}

export const states = [
  {
    name: "Alabama",
    abbreviation: "AL",
  },
  {
    name: "Alaska",
    abbreviation: "AK",
  },
  {
    name: "American Samoa",
    abbreviation: "AS",
  },
  {
    name: "Arizona",
    abbreviation: "AZ",
  },
  {
    name: "Arkansas",
    abbreviation: "AR",
  },
  {
    name: "California",
    abbreviation: "CA",
  },
  {
    name: "Colorado",
    abbreviation: "CO",
  },
  {
    name: "Connecticut",
    abbreviation: "CT",
  },
  {
    name: "Delaware",
    abbreviation: "DE",
  },
  {
    name: "District Of Columbia",
    abbreviation: "DC",
  },
  {
    name: "Federated States Of Micronesia",
    abbreviation: "FM",
  },
  {
    name: "Florida",
    abbreviation: "FL",
  },
  {
    name: "Georgia",
    abbreviation: "GA",
  },
  {
    name: "Guam",
    abbreviation: "GU",
  },
  {
    name: "Hawaii",
    abbreviation: "HI",
  },
  {
    name: "Idaho",
    abbreviation: "ID",
  },
  {
    name: "Illinois",
    abbreviation: "IL",
  },
  {
    name: "Indiana",
    abbreviation: "IN",
  },
  {
    name: "Iowa",
    abbreviation: "IA",
  },
  {
    name: "Kansas",
    abbreviation: "KS",
  },
  {
    name: "Kentucky",
    abbreviation: "KY",
  },
  {
    name: "Louisiana",
    abbreviation: "LA",
  },
  {
    name: "Maine",
    abbreviation: "ME",
  },
  {
    name: "Marshall Islands",
    abbreviation: "MH",
  },
  {
    name: "Maryland",
    abbreviation: "MD",
  },
  {
    name: "Massachusetts",
    abbreviation: "MA",
  },
  {
    name: "Michigan",
    abbreviation: "MI",
  },
  {
    name: "Minnesota",
    abbreviation: "MN",
  },
  {
    name: "Mississippi",
    abbreviation: "MS",
  },
  {
    name: "Missouri",
    abbreviation: "MO",
  },
  {
    name: "Montana",
    abbreviation: "MT",
  },
  {
    name: "Nebraska",
    abbreviation: "NE",
  },
  {
    name: "Nevada",
    abbreviation: "NV",
  },
  {
    name: "New Hampshire",
    abbreviation: "NH",
  },
  {
    name: "New Jersey",
    abbreviation: "NJ",
  },
  {
    name: "New Mexico",
    abbreviation: "NM",
  },
  {
    name: "New York",
    abbreviation: "NY",
  },
  {
    name: "North Carolina",
    abbreviation: "NC",
  },
  {
    name: "North Dakota",
    abbreviation: "ND",
  },
  {
    name: "Northern Mariana Islands",
    abbreviation: "MP",
  },
  {
    name: "Ohio",
    abbreviation: "OH",
  },
  {
    name: "Oklahoma",
    abbreviation: "OK",
  },
  {
    name: "Oregon",
    abbreviation: "OR",
  },
  {
    name: "Pennsylvania",
    abbreviation: "PA",
  },
  {
    name: "Puerto Rico",
    abbreviation: "PR",
  },
  {
    name: "Rhode Island",
    abbreviation: "RI",
  },
  {
    name: "South Carolina",
    abbreviation: "SC",
  },
  {
    name: "South Dakota",
    abbreviation: "SD",
  },
  {
    name: "Tennessee",
    abbreviation: "TN",
  },
  {
    name: "Texas",
    abbreviation: "TX",
  },
  {
    name: "Utah",
    abbreviation: "UT",
  },
  {
    name: "Vermont",
    abbreviation: "VT",
  },
  {
    name: "Virgin Islands",
    abbreviation: "VI",
  },
  {
    name: "Virginia",
    abbreviation: "VA",
  },
  {
    name: "Washington",
    abbreviation: "WA",
  },
  {
    name: "West Virginia",
    abbreviation: "WV",
  },
  {
    name: "Wisconsin",
    abbreviation: "WI",
  },
  {
    name: "Wyoming",
    abbreviation: "WY",
  },
] as const;

export type StateName = (typeof states)[number]["name"];
export type StateAbbreviation = (typeof states)[number]["abbreviation"];
export interface State {
  name: StateName;
  abbreviation: StateAbbreviation;
}

Press Enter to start editing.

Setting a default value

If the value does not need to be controlled but one of the options should be selected by default, set the defaultValue prop instead.

See Typescript Typing for type behavior.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function SettingADefaultValueExample(): ReactElement {
  const defaultValue: State = states[9];
  return (
    <Autocomplete
      label="State"
      options={states}
      defaultValue={defaultValue}
      listboxLabel="States"
    />
  );
}

export const states = [
  {
    name: "Alabama",
    abbreviation: "AL",
  },
  {
    name: "Alaska",
    abbreviation: "AK",
  },
  {
    name: "American Samoa",
    abbreviation: "AS",
  },
  {
    name: "Arizona",
    abbreviation: "AZ",
  },
  {
    name: "Arkansas",
    abbreviation: "AR",
  },
  {
    name: "California",
    abbreviation: "CA",
  },
  {
    name: "Colorado",
    abbreviation: "CO",
  },
  {
    name: "Connecticut",
    abbreviation: "CT",
  },
  {
    name: "Delaware",
    abbreviation: "DE",
  },
  {
    name: "District Of Columbia",
    abbreviation: "DC",
  },
  {
    name: "Federated States Of Micronesia",
    abbreviation: "FM",
  },
  {
    name: "Florida",
    abbreviation: "FL",
  },
  {
    name: "Georgia",
    abbreviation: "GA",
  },
  {
    name: "Guam",
    abbreviation: "GU",
  },
  {
    name: "Hawaii",
    abbreviation: "HI",
  },
  {
    name: "Idaho",
    abbreviation: "ID",
  },
  {
    name: "Illinois",
    abbreviation: "IL",
  },
  {
    name: "Indiana",
    abbreviation: "IN",
  },
  {
    name: "Iowa",
    abbreviation: "IA",
  },
  {
    name: "Kansas",
    abbreviation: "KS",
  },
  {
    name: "Kentucky",
    abbreviation: "KY",
  },
  {
    name: "Louisiana",
    abbreviation: "LA",
  },
  {
    name: "Maine",
    abbreviation: "ME",
  },
  {
    name: "Marshall Islands",
    abbreviation: "MH",
  },
  {
    name: "Maryland",
    abbreviation: "MD",
  },
  {
    name: "Massachusetts",
    abbreviation: "MA",
  },
  {
    name: "Michigan",
    abbreviation: "MI",
  },
  {
    name: "Minnesota",
    abbreviation: "MN",
  },
  {
    name: "Mississippi",
    abbreviation: "MS",
  },
  {
    name: "Missouri",
    abbreviation: "MO",
  },
  {
    name: "Montana",
    abbreviation: "MT",
  },
  {
    name: "Nebraska",
    abbreviation: "NE",
  },
  {
    name: "Nevada",
    abbreviation: "NV",
  },
  {
    name: "New Hampshire",
    abbreviation: "NH",
  },
  {
    name: "New Jersey",
    abbreviation: "NJ",
  },
  {
    name: "New Mexico",
    abbreviation: "NM",
  },
  {
    name: "New York",
    abbreviation: "NY",
  },
  {
    name: "North Carolina",
    abbreviation: "NC",
  },
  {
    name: "North Dakota",
    abbreviation: "ND",
  },
  {
    name: "Northern Mariana Islands",
    abbreviation: "MP",
  },
  {
    name: "Ohio",
    abbreviation: "OH",
  },
  {
    name: "Oklahoma",
    abbreviation: "OK",
  },
  {
    name: "Oregon",
    abbreviation: "OR",
  },
  {
    name: "Pennsylvania",
    abbreviation: "PA",
  },
  {
    name: "Puerto Rico",
    abbreviation: "PR",
  },
  {
    name: "Rhode Island",
    abbreviation: "RI",
  },
  {
    name: "South Carolina",
    abbreviation: "SC",
  },
  {
    name: "South Dakota",
    abbreviation: "SD",
  },
  {
    name: "Tennessee",
    abbreviation: "TN",
  },
  {
    name: "Texas",
    abbreviation: "TX",
  },
  {
    name: "Utah",
    abbreviation: "UT",
  },
  {
    name: "Vermont",
    abbreviation: "VT",
  },
  {
    name: "Virgin Islands",
    abbreviation: "VI",
  },
  {
    name: "Virginia",
    abbreviation: "VA",
  },
  {
    name: "Washington",
    abbreviation: "WA",
  },
  {
    name: "West Virginia",
    abbreviation: "WV",
  },
  {
    name: "Wisconsin",
    abbreviation: "WI",
  },
  {
    name: "Wyoming",
    abbreviation: "WY",
  },
] as const;

export type StateName = (typeof states)[number]["name"];
export type StateAbbreviation = (typeof states)[number]["abbreviation"];
export interface State {
  name: StateName;
  abbreviation: StateAbbreviation;
}

Press Enter to start editing.

Getting the current value

It is recommended to control the value instead of using this option.

When the value does not need to be stored anywhere but can be used for other actions, the onValueChange prop can be used.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function GettingTheCurrentValueExample(): ReactElement {
  const defaultValue: State = states[9];

  return (
    <Autocomplete
      label="State"
      options={states}
      defaultValue={defaultValue}
      listboxLabel="States"
      onValueChange={(value) => {
        // Do something with the value. Should generally not call `setState`

        // eslint-disable-next-line no-console
        console.log("value:", value);
      }}
    />
  );
}

export const states = [
  {
    name: "Alabama",
    abbreviation: "AL",
  },
  {
    name: "Alaska",
    abbreviation: "AK",
  },
  {
    name: "American Samoa",
    abbreviation: "AS",
  },
  {
    name: "Arizona",
    abbreviation: "AZ",
  },
  {
    name: "Arkansas",
    abbreviation: "AR",
  },
  {
    name: "California",
    abbreviation: "CA",
  },
  {
    name: "Colorado",
    abbreviation: "CO",
  },
  {
    name: "Connecticut",
    abbreviation: "CT",
  },
  {
    name: "Delaware",
    abbreviation: "DE",
  },
  {
    name: "District Of Columbia",
    abbreviation: "DC",
  },
  {
    name: "Federated States Of Micronesia",
    abbreviation: "FM",
  },
  {
    name: "Florida",
    abbreviation: "FL",
  },
  {
    name: "Georgia",
    abbreviation: "GA",
  },
  {
    name: "Guam",
    abbreviation: "GU",
  },
  {
    name: "Hawaii",
    abbreviation: "HI",
  },
  {
    name: "Idaho",
    abbreviation: "ID",
  },
  {
    name: "Illinois",
    abbreviation: "IL",
  },
  {
    name: "Indiana",
    abbreviation: "IN",
  },
  {
    name: "Iowa",
    abbreviation: "IA",
  },
  {
    name: "Kansas",
    abbreviation: "KS",
  },
  {
    name: "Kentucky",
    abbreviation: "KY",
  },
  {
    name: "Louisiana",
    abbreviation: "LA",
  },
  {
    name: "Maine",
    abbreviation: "ME",
  },
  {
    name: "Marshall Islands",
    abbreviation: "MH",
  },
  {
    name: "Maryland",
    abbreviation: "MD",
  },
  {
    name: "Massachusetts",
    abbreviation: "MA",
  },
  {
    name: "Michigan",
    abbreviation: "MI",
  },
  {
    name: "Minnesota",
    abbreviation: "MN",
  },
  {
    name: "Mississippi",
    abbreviation: "MS",
  },
  {
    name: "Missouri",
    abbreviation: "MO",
  },
  {
    name: "Montana",
    abbreviation: "MT",
  },
  {
    name: "Nebraska",
    abbreviation: "NE",
  },
  {
    name: "Nevada",
    abbreviation: "NV",
  },
  {
    name: "New Hampshire",
    abbreviation: "NH",
  },
  {
    name: "New Jersey",
    abbreviation: "NJ",
  },
  {
    name: "New Mexico",
    abbreviation: "NM",
  },
  {
    name: "New York",
    abbreviation: "NY",
  },
  {
    name: "North Carolina",
    abbreviation: "NC",
  },
  {
    name: "North Dakota",
    abbreviation: "ND",
  },
  {
    name: "Northern Mariana Islands",
    abbreviation: "MP",
  },
  {
    name: "Ohio",
    abbreviation: "OH",
  },
  {
    name: "Oklahoma",
    abbreviation: "OK",
  },
  {
    name: "Oregon",
    abbreviation: "OR",
  },
  {
    name: "Pennsylvania",
    abbreviation: "PA",
  },
  {
    name: "Puerto Rico",
    abbreviation: "PR",
  },
  {
    name: "Rhode Island",
    abbreviation: "RI",
  },
  {
    name: "South Carolina",
    abbreviation: "SC",
  },
  {
    name: "South Dakota",
    abbreviation: "SD",
  },
  {
    name: "Tennessee",
    abbreviation: "TN",
  },
  {
    name: "Texas",
    abbreviation: "TX",
  },
  {
    name: "Utah",
    abbreviation: "UT",
  },
  {
    name: "Vermont",
    abbreviation: "VT",
  },
  {
    name: "Virgin Islands",
    abbreviation: "VI",
  },
  {
    name: "Virginia",
    abbreviation: "VA",
  },
  {
    name: "Washington",
    abbreviation: "WA",
  },
  {
    name: "West Virginia",
    abbreviation: "WV",
  },
  {
    name: "Wisconsin",
    abbreviation: "WI",
  },
  {
    name: "Wyoming",
    abbreviation: "WY",
  },
] as const;

export type StateName = (typeof states)[number]["name"];
export type StateAbbreviation = (typeof states)[number]["abbreviation"];
export interface State {
  name: StateName;
  abbreviation: StateAbbreviation;
}

Press Enter to start editing.

Controlling the input value

If the input value needs to be controlled, provide a query and setQuery prop.

Query: ""

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { Typography } from "@react-md/core/typography/Typography";
import { type ReactElement, useState } from "react";

export default function ControllingTheInputValueExample(): ReactElement {
  const [query, setQuery] = useState("");

  return (
    <Box align="start" stacked>
      <Autocomplete
        label="State"
        query={query}
        setQuery={setQuery}
        options={states}
        listboxLabel="States"
      />
      <Button
        onClick={() => {
          setQuery("some other value");
        }}
        themeType="outline"
      >
        Set query
      </Button>
      <Typography>Query: {`"${query}"`}</Typography>
    </Box>
  );
}

export const states = [
  {
    name: "Alabama",
    abbreviation: "AL",
  },
  {
    name: "Alaska",
    abbreviation: "AK",
  },
  {
    name: "American Samoa",
    abbreviation: "AS",
  },
  {
    name: "Arizona",
    abbreviation: "AZ",
  },
  {
    name: "Arkansas",
    abbreviation: "AR",
  },
  {
    name: "California",
    abbreviation: "CA",
  },
  {
    name: "Colorado",
    abbreviation: "CO",
  },
  {
    name: "Connecticut",
    abbreviation: "CT",
  },
  {
    name: "Delaware",
    abbreviation: "DE",
  },
  {
    name: "District Of Columbia",
    abbreviation: "DC",
  },
  {
    name: "Federated States Of Micronesia",
    abbreviation: "FM",
  },
  {
    name: "Florida",
    abbreviation: "FL",
  },
  {
    name: "Georgia",
    abbreviation: "GA",
  },
  {
    name: "Guam",
    abbreviation: "GU",
  },
  {
    name: "Hawaii",
    abbreviation: "HI",
  },
  {
    name: "Idaho",
    abbreviation: "ID",
  },
  {
    name: "Illinois",
    abbreviation: "IL",
  },
  {
    name: "Indiana",
    abbreviation: "IN",
  },
  {
    name: "Iowa",
    abbreviation: "IA",
  },
  {
    name: "Kansas",
    abbreviation: "KS",
  },
  {
    name: "Kentucky",
    abbreviation: "KY",
  },
  {
    name: "Louisiana",
    abbreviation: "LA",
  },
  {
    name: "Maine",
    abbreviation: "ME",
  },
  {
    name: "Marshall Islands",
    abbreviation: "MH",
  },
  {
    name: "Maryland",
    abbreviation: "MD",
  },
  {
    name: "Massachusetts",
    abbreviation: "MA",
  },
  {
    name: "Michigan",
    abbreviation: "MI",
  },
  {
    name: "Minnesota",
    abbreviation: "MN",
  },
  {
    name: "Mississippi",
    abbreviation: "MS",
  },
  {
    name: "Missouri",
    abbreviation: "MO",
  },
  {
    name: "Montana",
    abbreviation: "MT",
  },
  {
    name: "Nebraska",
    abbreviation: "NE",
  },
  {
    name: "Nevada",
    abbreviation: "NV",
  },
  {
    name: "New Hampshire",
    abbreviation: "NH",
  },
  {
    name: "New Jersey",
    abbreviation: "NJ",
  },
  {
    name: "New Mexico",
    abbreviation: "NM",
  },
  {
    name: "New York",
    abbreviation: "NY",
  },
  {
    name: "North Carolina",
    abbreviation: "NC",
  },
  {
    name: "North Dakota",
    abbreviation: "ND",
  },
  {
    name: "Northern Mariana Islands",
    abbreviation: "MP",
  },
  {
    name: "Ohio",
    abbreviation: "OH",
  },
  {
    name: "Oklahoma",
    abbreviation: "OK",
  },
  {
    name: "Oregon",
    abbreviation: "OR",
  },
  {
    name: "Pennsylvania",
    abbreviation: "PA",
  },
  {
    name: "Puerto Rico",
    abbreviation: "PR",
  },
  {
    name: "Rhode Island",
    abbreviation: "RI",
  },
  {
    name: "South Carolina",
    abbreviation: "SC",
  },
  {
    name: "South Dakota",
    abbreviation: "SD",
  },
  {
    name: "Tennessee",
    abbreviation: "TN",
  },
  {
    name: "Texas",
    abbreviation: "TX",
  },
  {
    name: "Utah",
    abbreviation: "UT",
  },
  {
    name: "Vermont",
    abbreviation: "VT",
  },
  {
    name: "Virgin Islands",
    abbreviation: "VI",
  },
  {
    name: "Virginia",
    abbreviation: "VA",
  },
  {
    name: "Washington",
    abbreviation: "WA",
  },
  {
    name: "West Virginia",
    abbreviation: "WV",
  },
  {
    name: "Wisconsin",
    abbreviation: "WI",
  },
  {
    name: "Wyoming",
    abbreviation: "WY",
  },
] as const;

export type StateName = (typeof states)[number]["name"];
export type StateAbbreviation = (typeof states)[number]["abbreviation"];
export interface State {
  name: StateName;
  abbreviation: StateAbbreviation;
}

Press Enter to start editing.

Getting the current input value

If the input value doesn't need to be controlled, the value can be retrieved with the normal onChange event handler.

"use client";

/* eslint-disable no-console */
import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function GettingTheCurrentInputValueExample(): ReactElement {
  return (
    <Autocomplete
      label="Fruit"
      placeholder="Apple"
      listboxLabel="Fruits"
      options={fruits}
      onChange={(event) => {
        // do something with the current value
        const { value } = event.currentTarget;
        console.log("value: ", value);
      }}
    />
  );
}

export const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blueberry",
  "Cranberry",
  "Kiwi",
  "Mango",
  "Orange",
  "Peach",
  "Plum",
  "Strawberry",
];

Press Enter to start editing.

Disable Filtering

There are some cases where the available options should only be suggestions and not required value such as displaying the most recent searches. Set the filter prop to noopAutocompleteFilter to enable this behavior which also enables the allowAnyValue prop.

This is the default behavior when type="search" is set.

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { noopAutocompleteFilter } from "@react-md/core/autocomplete/defaults";
import { type ReactElement } from "react";

export default function DisableFilteringExample(): ReactElement {
  return (
    <Autocomplete
      label="Fruit"
      placeholder="Apple"
      listboxLabel="Fruits"
      options={fruits}
      filter={noopAutocompleteFilter}
    />
  );
}

export const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blueberry",
  "Cranberry",
  "Kiwi",
  "Mango",
  "Orange",
  "Peach",
  "Plum",
  "Strawberry",
];

Press Enter to start editing.

Custom Filter Function

The Autocomplete uses the caseInsensitiveSearch filter function by default but can be changed using the filter prop. The filter function must return the filtered list and is called with an object containing:

This example will show how the filter function could be swapped out for the fuzzySearch.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type AutocompleteFilterFunction } from "@react-md/core/autocomplete/types";
import { caseInsensitiveSearch } from "@react-md/core/searching/caseInsensitive";
import { fuzzySearch } from "@react-md/core/searching/fuzzy";
import { type ReactElement } from "react";
const NO_STARTS_WITH = false;

const filter: AutocompleteFilterFunction<State> = (options) => {
  // these are all provided
  // const { list, query, extractor } = options

  // The default filter behavior is to require each option to start with
  // the query to match, so here's an example allow matching anywhere
  if (NO_STARTS_WITH) {
    return caseInsensitiveSearch(options);
  }

  // this is about the same as the caseInsensitiveSearch, but the letters
  // do not need to appear next to each other to match
  return fuzzySearch(options);
};

export default function CustomFilterFunctionExample(): ReactElement {
  return (
    <Autocomplete
      label="State"
      options={states}
      listboxLabel="States"
      filter={filter}
    />
  );
}

export const states = [
  {
    name: "Alabama",
    abbreviation: "AL",
  },
  {
    name: "Alaska",
    abbreviation: "AK",
  },
  {
    name: "American Samoa",
    abbreviation: "AS",
  },
  {
    name: "Arizona",
    abbreviation: "AZ",
  },
  {
    name: "Arkansas",
    abbreviation: "AR",
  },
  {
    name: "California",
    abbreviation: "CA",
  },
  {
    name: "Colorado",
    abbreviation: "CO",
  },
  {
    name: "Connecticut",
    abbreviation: "CT",
  },
  {
    name: "Delaware",
    abbreviation: "DE",
  },
  {
    name: "District Of Columbia",
    abbreviation: "DC",
  },
  {
    name: "Federated States Of Micronesia",
    abbreviation: "FM",
  },
  {
    name: "Florida",
    abbreviation: "FL",
  },
  {
    name: "Georgia",
    abbreviation: "GA",
  },
  {
    name: "Guam",
    abbreviation: "GU",
  },
  {
    name: "Hawaii",
    abbreviation: "HI",
  },
  {
    name: "Idaho",
    abbreviation: "ID",
  },
  {
    name: "Illinois",
    abbreviation: "IL",
  },
  {
    name: "Indiana",
    abbreviation: "IN",
  },
  {
    name: "Iowa",
    abbreviation: "IA",
  },
  {
    name: "Kansas",
    abbreviation: "KS",
  },
  {
    name: "Kentucky",
    abbreviation: "KY",
  },
  {
    name: "Louisiana",
    abbreviation: "LA",
  },
  {
    name: "Maine",
    abbreviation: "ME",
  },
  {
    name: "Marshall Islands",
    abbreviation: "MH",
  },
  {
    name: "Maryland",
    abbreviation: "MD",
  },
  {
    name: "Massachusetts",
    abbreviation: "MA",
  },
  {
    name: "Michigan",
    abbreviation: "MI",
  },
  {
    name: "Minnesota",
    abbreviation: "MN",
  },
  {
    name: "Mississippi",
    abbreviation: "MS",
  },
  {
    name: "Missouri",
    abbreviation: "MO",
  },
  {
    name: "Montana",
    abbreviation: "MT",
  },
  {
    name: "Nebraska",
    abbreviation: "NE",
  },
  {
    name: "Nevada",
    abbreviation: "NV",
  },
  {
    name: "New Hampshire",
    abbreviation: "NH",
  },
  {
    name: "New Jersey",
    abbreviation: "NJ",
  },
  {
    name: "New Mexico",
    abbreviation: "NM",
  },
  {
    name: "New York",
    abbreviation: "NY",
  },
  {
    name: "North Carolina",
    abbreviation: "NC",
  },
  {
    name: "North Dakota",
    abbreviation: "ND",
  },
  {
    name: "Northern Mariana Islands",
    abbreviation: "MP",
  },
  {
    name: "Ohio",
    abbreviation: "OH",
  },
  {
    name: "Oklahoma",
    abbreviation: "OK",
  },
  {
    name: "Oregon",
    abbreviation: "OR",
  },
  {
    name: "Pennsylvania",
    abbreviation: "PA",
  },
  {
    name: "Puerto Rico",
    abbreviation: "PR",
  },
  {
    name: "Rhode Island",
    abbreviation: "RI",
  },
  {
    name: "South Carolina",
    abbreviation: "SC",
  },
  {
    name: "South Dakota",
    abbreviation: "SD",
  },
  {
    name: "Tennessee",
    abbreviation: "TN",
  },
  {
    name: "Texas",
    abbreviation: "TX",
  },
  {
    name: "Utah",
    abbreviation: "UT",
  },
  {
    name: "Vermont",
    abbreviation: "VT",
  },
  {
    name: "Virgin Islands",
    abbreviation: "VI",
  },
  {
    name: "Virginia",
    abbreviation: "VA",
  },
  {
    name: "Washington",
    abbreviation: "WA",
  },
  {
    name: "West Virginia",
    abbreviation: "WV",
  },
  {
    name: "Wisconsin",
    abbreviation: "WI",
  },
  {
    name: "Wyoming",
    abbreviation: "WY",
  },
] as const;

export type StateName = (typeof states)[number]["name"];
export type StateAbbreviation = (typeof states)[number]["abbreviation"];
export interface State {
  name: StateName;
  abbreviation: StateAbbreviation;
}

Press Enter to start editing.

Allow Any Value

The Autocomplete normally behave like a searchable select component where it requires a specific option to be selected and will reject all other values. If any value should be allowed, enable the allowAnyValue prop.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function AllowAnyValueExample(): ReactElement {
  return (
    <Autocomplete
      label="Fruit"
      placeholder="Apple"
      listboxLabel="Fruits"
      options={fruits}
      allowAnyValue
    />
  );
}

export const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blueberry",
  "Cranberry",
  "Kiwi",
  "Mango",
  "Orange",
  "Peach",
  "Plum",
  "Strawberry",
];

Press Enter to start editing.

Creatable

An alternative to the allowAnyValue prop is to allow the user to create a new option with their typed value by manually selecting the dynamically generated option. There is nothing built-in to the Autocomplete component itself, but can be easily coded with a few changes:

This example will show how to insert a Add: "${query}" option with the list of fruits. The Add: "${query}" is set as the children so that when the option is selected, only the query will be inserted into the input.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { defaultAutocompleteFilter } from "@react-md/core/autocomplete/defaults";
import { type ReactElement } from "react";

export default function CreatableAutocompleteExample(): ReactElement {
  return (
    <Autocomplete
      label="Fruit"
      placeholder="Apple"
      listboxLabel="Fruits"
      options={fruits.map((fruit) => ({
        label: fruit,
        value: fruit,
      }))}
      filter={(options) => {
        const { list, extractor, query } = options;
        const filtered = defaultAutocompleteFilter(options);
        if (query && !list.some((option) => extractor(option) === query)) {
          return [
            ...filtered,
            {
              label: query,
              value: query,
              children: `Add: "${query}"`,
            },
          ];
        }

        return filtered;
      }}
    />
  );
}

export const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blueberry",
  "Cranberry",
  "Kiwi",
  "Mango",
  "Orange",
  "Peach",
  "Plum",
  "Strawberry",
];

Press Enter to start editing.

Async Example

The Autocomplete supports rendering a CircularProgress after the input field to show async behavior. The CircularProgress will be shown while the loading prop is true.

This example below will "load" the options each time the autocomplete is opened.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { useUnmounted } from "@react-md/core/useUnmounted";
import { wait } from "@react-md/core/utils/wait";
import { type ReactElement, useState } from "react";

interface AsyncState {
  loading: boolean;
  options: readonly State[];
}

export default function AsyncExample(): ReactElement {
  const [{ loading, options }, setState] = useState<AsyncState>({
    loading: false,
    options: [],
  });
  const unmounted = useUnmounted();

  return (
    <Autocomplete
      label="State"
      options={options}
      listboxLabel="States"
      loading={loading}
      onOpen={async () => {
        setState({ loading: true, options: [] });
        await wait(800);
        if (!unmounted.current) {
          setState({
            loading: false,
            options: states,
          });
        }
      }}
      style={{ width: "18rem" }}
    />
  );
}

export const states = [
  {
    name: "Alabama",
    abbreviation: "AL",
  },
  {
    name: "Alaska",
    abbreviation: "AK",
  },
  {
    name: "American Samoa",
    abbreviation: "AS",
  },
  {
    name: "Arizona",
    abbreviation: "AZ",
  },
  {
    name: "Arkansas",
    abbreviation: "AR",
  },
  {
    name: "California",
    abbreviation: "CA",
  },
  {
    name: "Colorado",
    abbreviation: "CO",
  },
  {
    name: "Connecticut",
    abbreviation: "CT",
  },
  {
    name: "Delaware",
    abbreviation: "DE",
  },
  {
    name: "District Of Columbia",
    abbreviation: "DC",
  },
  {
    name: "Federated States Of Micronesia",
    abbreviation: "FM",
  },
  {
    name: "Florida",
    abbreviation: "FL",
  },
  {
    name: "Georgia",
    abbreviation: "GA",
  },
  {
    name: "Guam",
    abbreviation: "GU",
  },
  {
    name: "Hawaii",
    abbreviation: "HI",
  },
  {
    name: "Idaho",
    abbreviation: "ID",
  },
  {
    name: "Illinois",
    abbreviation: "IL",
  },
  {
    name: "Indiana",
    abbreviation: "IN",
  },
  {
    name: "Iowa",
    abbreviation: "IA",
  },
  {
    name: "Kansas",
    abbreviation: "KS",
  },
  {
    name: "Kentucky",
    abbreviation: "KY",
  },
  {
    name: "Louisiana",
    abbreviation: "LA",
  },
  {
    name: "Maine",
    abbreviation: "ME",
  },
  {
    name: "Marshall Islands",
    abbreviation: "MH",
  },
  {
    name: "Maryland",
    abbreviation: "MD",
  },
  {
    name: "Massachusetts",
    abbreviation: "MA",
  },
  {
    name: "Michigan",
    abbreviation: "MI",
  },
  {
    name: "Minnesota",
    abbreviation: "MN",
  },
  {
    name: "Mississippi",
    abbreviation: "MS",
  },
  {
    name: "Missouri",
    abbreviation: "MO",
  },
  {
    name: "Montana",
    abbreviation: "MT",
  },
  {
    name: "Nebraska",
    abbreviation: "NE",
  },
  {
    name: "Nevada",
    abbreviation: "NV",
  },
  {
    name: "New Hampshire",
    abbreviation: "NH",
  },
  {
    name: "New Jersey",
    abbreviation: "NJ",
  },
  {
    name: "New Mexico",
    abbreviation: "NM",
  },
  {
    name: "New York",
    abbreviation: "NY",
  },
  {
    name: "North Carolina",
    abbreviation: "NC",
  },
  {
    name: "North Dakota",
    abbreviation: "ND",
  },
  {
    name: "Northern Mariana Islands",
    abbreviation: "MP",
  },
  {
    name: "Ohio",
    abbreviation: "OH",
  },
  {
    name: "Oklahoma",
    abbreviation: "OK",
  },
  {
    name: "Oregon",
    abbreviation: "OR",
  },
  {
    name: "Pennsylvania",
    abbreviation: "PA",
  },
  {
    name: "Puerto Rico",
    abbreviation: "PR",
  },
  {
    name: "Rhode Island",
    abbreviation: "RI",
  },
  {
    name: "South Carolina",
    abbreviation: "SC",
  },
  {
    name: "South Dakota",
    abbreviation: "SD",
  },
  {
    name: "Tennessee",
    abbreviation: "TN",
  },
  {
    name: "Texas",
    abbreviation: "TX",
  },
  {
    name: "Utah",
    abbreviation: "UT",
  },
  {
    name: "Vermont",
    abbreviation: "VT",
  },
  {
    name: "Virgin Islands",
    abbreviation: "VI",
  },
  {
    name: "Virginia",
    abbreviation: "VA",
  },
  {
    name: "Washington",
    abbreviation: "WA",
  },
  {
    name: "West Virginia",
    abbreviation: "WV",
  },
  {
    name: "Wisconsin",
    abbreviation: "WI",
  },
  {
    name: "Wyoming",
    abbreviation: "WY",
  },
] as const;

export type StateName = (typeof states)[number]["name"];
export type StateAbbreviation = (typeof states)[number]["abbreviation"];
export interface State {
  name: StateName;
  abbreviation: StateAbbreviation;
}

Press Enter to start editing.

Debounced Search Example

When the Autocomplete should be used as a <input type="search" /> to send requests to an API as the user types to get the options, set the type="search" and add a custom onChange handler to send the current value to the search API. It is recommended to either debounce or throttle these search requests so that each keystroke is not a new request.

react-md provides two hooks to help with debouncing and throttling:

This example will show a debounced implementation while the next example will show a throttled implementation.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { useDebouncedFunction } from "@react-md/core/useDebouncedFunction";
import { useUnmounted } from "@react-md/core/useUnmounted";
import { wait } from "@react-md/core/utils/wait";
import { type ReactElement, useRef, useState } from "react";

export default function DebouncedSearchExample(): ReactElement {
  const [loading, setLoading] = useState(false);
  const [options, setOptions] = useState<readonly string[]>([]);
  const query = useRef("");
  const unmounted = useUnmounted();
  const search = useDebouncedFunction(async function load(
    value: string,
  ): Promise<void> {
    query.current = value;
    await wait(1000);
    if (!unmounted.current) {
      setLoading(query.current !== value);
      setOptions(
        value.trim()
          ? Array.from({ length: 10 }, (_, i) => `${value} ${i + 1}`)
          : [],
      );
    }
  }, 500);

  return (
    <Autocomplete
      type="search"
      label="Search"
      placeholder="Search..."
      listboxLabel="Resultsk"
      options={options}
      loading={loading}
      onChange={(event) => {
        setLoading(true);
        search(event.currentTarget.value);
      }}
      style={{ width: "18rem" }}
    />
  );
}

Press Enter to start editing.

Throttled Search Example

This is the same example as above, but using useThrottledFunction instead.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { useThrottledFunction } from "@react-md/core/useThrottledFunction";
import { useUnmounted } from "@react-md/core/useUnmounted";
import { wait } from "@react-md/core/utils/wait";
import { type ReactElement, useRef, useState } from "react";

export default function ThrottledSearchExample(): ReactElement {
  const [loading, setLoading] = useState(false);
  const [options, setOptions] = useState<readonly string[]>([]);
  const query = useRef("");
  const unmounted = useUnmounted();
  const search = useThrottledFunction(async function load(
    value: string,
  ): Promise<void> {
    query.current = value;
    await wait(1000);
    if (!unmounted.current) {
      setLoading(query.current !== value);
      setOptions(
        value.trim()
          ? Array.from({ length: 10 }, (_, i) => `${value} ${i + 1}`)
          : [],
      );
    }
  }, 300);

  return (
    <Autocomplete
      type="search"
      label="Search"
      placeholder="Search..."
      listboxLabel="Resultsk"
      options={options}
      loading={loading}
      onChange={(event) => {
        setLoading(true);
        void search(event.currentTarget.value);
      }}
      style={{ width: "18rem" }}
    />
  );
}

Press Enter to start editing.

Multiple Values

The Autocomplete supports multiple values by either:

When multiple values are allowed, the Autocomplete will display inline chips representing all the selected values and automatically clear the input when a new option is selected.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement, useState } from "react";

export default function MultipleValuesExample(): ReactElement {
  const [value, setValue] = useState<readonly Dessert[]>([]);
  return (
    <>
      <Autocomplete
        label="Dessert"
        placeholder="Ice cream"
        value={value}
        setValue={setValue}
        listboxLabel="Desserts"
        options={desserts}
      />
    </>
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Disable Close On Select

To prevent the listbox from closing when an option is selected, enable the disableCloseOnSelect prop.

This is also supported for the single select Autocomplete

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement, useState } from "react";

export default function DisableCloseOnSelectExample(): ReactElement {
  const [value, setValue] = useState<readonly Dessert[]>([]);
  return (
    <>
      <Autocomplete
        label="Dessert"
        placeholder="Ice cream"
        value={value}
        setValue={setValue}
        listboxLabel="Desserts"
        options={desserts}
        disableCloseOnSelect
      />
    </>
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Checkboxes

The multiselect Autocomplete also supports updating all the options to use checkbox icons by enabling the checkboxes prop. This also enables the disableCloseOnSelect by default.

The Option will use the ICON_CONFIG.checkbox and ICON_CONFIG.checkboxChecked icons for the unselected and selected states.

These icons can be overridden using the getOptionProps and returning a custom selectedIcon / unselectedIcon.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement, useState } from "react";

export default function MultiselectCheckboxesExample(): ReactElement {
  const [value, setValue] = useState<readonly Dessert[]>([]);
  return (
    <>
      <Autocomplete
        label="Dessert"
        placeholder="Ice cream"
        value={value}
        setValue={setValue}
        listboxLabel="Desserts"
        checkboxes
        options={desserts}
      />
    </>
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Disable Inline Chips

If the selected options should not be shown as inline chips, enable the disableInlineChips prop.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { AutocompleteChip } from "@react-md/core/autocomplete/AutocompleteChip";
import { Box } from "@react-md/core/box/Box";
import { type ReactElement, useState } from "react";

export default function DisableInlineChipsExample(): ReactElement {
  const [value, setValue] = useState<readonly Dessert[]>([
    desserts[1],
    desserts[2],
  ]);

  return (
    <>
      <Box
        role="status"
        aria-live="polite"
        aria-label="Selected Desserts"
        align="start"
        fullWidth
        disablePadding
      >
        {value.map((dessert) => (
          <AutocompleteChip
            key={dessert.name}
            theme="outline"
            onClick={() => {
              setValue((prevValue) => prevValue.filter((d) => d !== dessert));
            }}
          >
            {dessert.name}
          </AutocompleteChip>
        ))}
      </Box>
      <Autocomplete
        label="Dessert"
        placeholder="Ice cream"
        value={value}
        setValue={setValue}
        listboxLabel="Desserts"
        options={desserts}
        disableInlineChips
      />
    </>
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Filter Selected

The selected options can also be removed from the listbox by enabling the filterSelected prop. It is only recommended to enable this prop when the disableInlineChips is enabled.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { AutocompleteChip } from "@react-md/core/autocomplete/AutocompleteChip";
import { Box } from "@react-md/core/box/Box";
import { type ReactElement, useState } from "react";

export default function FilterSelectedExample(): ReactElement {
  const [value, setValue] = useState<readonly Dessert[]>([
    desserts[1],
    desserts[2],
  ]);

  return (
    <>
      <Box
        role="status"
        aria-live="polite"
        aria-label="Selected Desserts"
        align="start"
        fullWidth
        disablePadding
      >
        {value.map((dessert) => (
          <AutocompleteChip
            key={dessert.name}
            theme="outline"
            onClick={() => {
              setValue((prevValue) => prevValue.filter((d) => d !== dessert));
            }}
          >
            {dessert.name}
          </AutocompleteChip>
        ))}
      </Box>
      <Autocomplete
        label="Dessert"
        placeholder="Ice cream"
        value={value}
        setValue={setValue}
        listboxLabel="Desserts"
        options={desserts}
        filterSelected
        disableInlineChips
      />
    </>
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Customizing Inline Chips

The inline chips can be customized using the getChipProps which provides the index and option being rendered. This can be used to disable chips, add custom styles, change the theme, etc.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import FavoriteIcon from "@react-md/material-icons/FavoriteIcon";
import { type ReactElement } from "react";

export default function CustomizingInlineChipsExample(): ReactElement {
  const isDisabled = (option: Dessert): boolean =>
    option === desserts[0] || option === desserts[1] || option === desserts[4];

  return (
    <>
      <Autocomplete
        label="Dessert"
        placeholder="Ice cream"
        defaultValue={[desserts[0], desserts[1], desserts[4]]}
        listboxLabel="Desserts"
        options={desserts}
        getOptionProps={({ option }) => {
          return {
            disabled: isDisabled(option),
          };
        }}
        getChipProps={({ option, index }) => {
          return {
            theme: "outline",
            leftAddon: <FavoriteIcon />,
            disabled: isDisabled(option),
            children: `${option.name} ${index + 1}`,
          };
        }}
      />
    </>
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Customization

Highlights

The search query can be highlighted using the HighlightText component and the getOptionProps to provide highlighted children to each option.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { caseInsensitiveSearch } from "@react-md/core/searching/caseInsensitive";
import { HighlightText } from "@react-md/core/typography/HighlightText";
import { type ReactElement } from "react";

export default function HighlightsExample(): ReactElement {
  return (
    <Autocomplete
      label="Fruit"
      placeholder="Apple"
      listboxLabel="Fruits"
      options={desserts}
      filter={(options) => caseInsensitiveSearch(options)}
      getOptionProps={(options) => {
        const { option, query } = options;

        return {
          children: <HighlightText query={query}>{option.name}</HighlightText>,
        };
      }}
    />
  );
}

export interface Dessert {
  name: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
  sodium: number;
  calcium: number;
  iron: number;
  type: "Ice cream" | "Pastry" | "Other";
}

export type DessertKey = keyof Dessert;

export const desserts: readonly Dessert[] = [
  {
    name: "Frozen yogurt",
    type: "Ice cream",
    calories: 159,
    fat: 6.0,
    carbs: 24,
    protein: 4.0,
    sodium: 87,
    calcium: 14,
    iron: 1,
  },
  {
    name: "Ice cream sandwich",
    type: "Ice cream",
    calories: 237,
    fat: 9.0,
    carbs: 37,
    protein: 4.3,
    sodium: 129,
    calcium: 8,
    iron: 1,
  },
  {
    name: "Eclair",
    type: "Pastry",
    calories: 262,
    fat: 16.0,
    carbs: 37,
    protein: 6.0,
    sodium: 337,
    calcium: 6,
    iron: 7,
  },
  {
    name: "Cupcake",
    type: "Pastry",
    calories: 305,
    fat: 3.7,
    carbs: 67,
    protein: 4.3,
    sodium: 413,
    calcium: 3,
    iron: 8,
  },
  {
    name: "Gingerbread",
    type: "Pastry",
    calories: 356,
    fat: 16.0,
    carbs: 49,
    protein: 3.9,
    sodium: 327,
    calcium: 7,
    iron: 16,
  },
  {
    name: "Jelly bean",
    type: "Other",
    calories: 375,
    fat: 0.0,
    carbs: 94,
    protein: 0.0,
    sodium: 50,
    calcium: 0,
    iron: 0,
  },
  {
    name: "Lollipop",
    type: "Other",
    calories: 392,
    fat: 0.2,
    carbs: 98,
    protein: 0.0,
    sodium: 38,
    calcium: 0,
    iron: 2,
  },
  {
    name: "Honeycomb",
    type: "Other",
    calories: 408,
    fat: 3.2,
    carbs: 87,
    protein: 6.5,
    sodium: 562,
    calcium: 0,
    iron: 45,
  },
  {
    name: "Donut",
    type: "Pastry",
    calories: 52,
    fat: 25.0,
    carbs: 51,
    protein: 4.9,
    sodium: 326,
    calcium: 2,
    iron: 22,
  },
  {
    name: "KitKat",
    type: "Other",
    calories: 16,
    fat: 6.0,
    carbs: 65,
    protein: 7.0,
    sodium: 54,
    calcium: 12,
    iron: 6,
  },
];

export const dessertColumns = Object.keys(desserts[0]) as readonly DessertKey[];

Press Enter to start editing.

Disabling the Clear and Dropdown Buttons

The clear button and dropdown button can be removed by enabling the disableClearButton and disableDropdownButton props respectively. They do not need to be used in tandem.

The keyboard shortcuts for clearing the input and showing/hiding the menu will still be enabled if these props are enabled.

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function DisablingTheClearAndDropdownButtonsExample(): ReactElement {
  return (
    <Autocomplete
      label="Fruit"
      placeholder="Apple"
      listboxLabel="Fruits"
      options={fruits}
      disableClearButton
      disableDropdownButton
    />
  );
}

export const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blueberry",
  "Cranberry",
  "Kiwi",
  "Mango",
  "Orange",
  "Peach",
  "Plum",
  "Strawberry",
];

Press Enter to start editing.

Github's Label Picker

This is a more complex example that will showcase most of the customization options available in the Autocomplete component and other child components.

This demo shows how the Github label picker could be created with react-md with most of the same behavior. Here's a breakdown of how this was created:

None yet

"use client";

import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { Avatar } from "@react-md/core/avatar/Avatar";
import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { Chip } from "@react-md/core/chip/Chip";
import { FixedDialog } from "@react-md/core/dialog/FixedDialog";
import { DEFAULT_OPTION_UNSELECTED_ICON } from "@react-md/core/form/Option";
import { List } from "@react-md/core/list/List";
import { ListItem } from "@react-md/core/list/ListItem";
import { ListSubheader } from "@react-md/core/list/ListSubheader";
import { BELOW_INNER_RIGHT_ANCHOR } from "@react-md/core/positioning/constants";
import { caseInsensitiveSearch } from "@react-md/core/searching/caseInsensitive";
import { contrastColor } from "@react-md/core/theme/utils";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import CloseIcon from "@react-md/material-icons/CloseIcon";
import EditIcon from "@react-md/material-icons/EditIcon";
import SettingsIcon from "@react-md/material-icons/SettingsIcon";
import {
  type KeyboardEvent,
  type ReactElement,
  useId,
  useRef,
  useState,
} from "react";
import styles from "./GithubLabelPickerExample.module.scss";

const noop = (): void => {
  // do nothing
};

export default function GithubLabelPickerExample(): ReactElement {
  const {
    toggled: visible,
    enable: show,
    disable: onRequestClose,
  } = useToggle();
  const fixedTo = useRef<HTMLButtonElement>(null);
  const titleId = useId();
  const [labels, setLabels] = useState<readonly GithubLabel[]>([]);
  const nextValue = useRef(labels);

  // this is only required since the `setVisible` behavior is set to a no-op
  // function to enforce the listbox is always visible
  const handleKeyDown = (event: KeyboardEvent): void => {
    if (event.key === "Escape") {
      onRequestClose();
    }
  };

  return (
    <div className={styles.container}>
      <Button
        ref={fixedTo}
        onClick={show}
        className={styles.button}
        disableRipple
      >
        Labels <SettingsIcon />
      </Button>
      <Box disablePadding>
        {!labels.length && <Typography margin="none">None yet</Typography>}
        {labels.map(({ name, color }) => (
          <Chip
            key={name}
            style={{
              background: color,
              color: contrastColor(color),
            }}
            disableRipple
          >
            {name}
          </Chip>
        ))}
      </Box>
      <FixedDialog
        aria-labelledby={titleId}
        anchor={BELOW_INNER_RIGHT_ANCHOR}
        visible={visible}
        fixedTo={fixedTo}
        onRequestClose={onRequestClose}
        disableTransition
        onKeyDown={handleKeyDown}
        onExited={() => {
          setLabels(nextValue.current);
        }}
        className={styles.dialog}
      >
        <Typography
          id={titleId}
          as="h5"
          type="caption"
          margin="none"
          className={styles.title}
          textColor="text-primary"
        >
          Apply labels to this issue
        </Typography>
        <Autocomplete
          aria-label="Labels"
          theme="outline"
          autoFocus
          placeholder="Filter labels"
          options={[...githubLabels].sort((a, b) => {
            // sort the selected labels first
            let aIndex = labels.indexOf(a);
            if (aIndex === -1) {
              aIndex = labels.length + githubLabels.indexOf(a);
            }

            let bIndex = labels.indexOf(b);
            if (bIndex === -1) {
              bIndex = labels.length + githubLabels.indexOf(a);
            }

            return aIndex - bIndex;
          })}
          defaultValue={labels}
          onValueChange={(value) => {
            nextValue.current = value;
          }}
          className={styles.autocomplete}
          listboxLabel="Labels"
          disableInlineChips
          disableCloseOnSelect
          disableClearButton
          disableDropdownButton
          updateQueryOnSelect="as-is"
          onKeyDown={handleKeyDown}
          filter={(options) => caseInsensitiveSearch(options)}
          listboxProps={{
            disablePortal: true,
            disableElevation: true,
            disableTransition: true,
            disableSelectedIcon: false,
            disableFixedPositioning: true,
            onKeyDown: handleKeyDown,
          }}
          getOptionProps={({ option, selected }) => {
            return {
              height: "auto",
              className: styles.option,
              rightAddon: selected && <CloseIcon />,
              disableRipple: true,
              children: (
                <Box disablePadding>
                  <Avatar style={{ background: option.color }} />
                  {option.name}
                </Box>
              ),
            };
          }}
          visible
          setVisible={noop}
          // this would really be a dynamic creatable thing to match github, but too much for this demo
          noOptionsChildren={<ListSubheader>No labels</ListSubheader>}
        />
        <List>
          <ListItem
            height="auto"
            className={styles.editLabels}
            leftAddon={DEFAULT_OPTION_UNSELECTED_ICON}
            disableLeftAddonSpacing
            disableTextChildren
          >
            <Box disablePadding>
              <EditIcon />
              <span>Edit labels</span>
            </Box>
          </ListItem>
        </List>
      </FixedDialog>
    </div>
  );
}

export interface GithubLabel {
  name: string;
  color: string;
}

export const githubLabels: readonly GithubLabel[] = [
  {
    name: "bug",
    color: "#fc2929",
  },
  {
    name: "enhancement",
    color: "#84b6eb",
  },
  {
    name: "duplicate",
    color: "#cccccc",
  },
  {
    name: "help wanted",
    color: "#159818",
  },
  {
    name: "invalid",
    color: "#e6e6e6",
  },
  {
    name: "question",
    color: "#cc317c",
  },
  {
    name: "wontfix",
    color: "#ffffff",
  },
];

Press Enter to start editing.

@use "everything";

.container {
  @include everything.box-set-var(gap, 0.25rem);
  @include everything.chip-set-var(height, 1.25rem);
  @include everything.chip-set-var(horizontal-padding, 0.5rem);

  max-width: 16rem;
  width: 100%;

  :global(.rmd-chip) {
    font-size: 0.75rem;
  }
}

.button {
  @include everything.interaction-set-var(hover-background-color, transparent);

  justify-content: space-between;
  padding: 0;
  width: 100%;

  @include everything.mouse-hover {
    color: #4493f8;
  }
}

.dialog {
  // I just grabbed the color variables in dark mode and moved them here. not
  // going to do both light/dark mode
  @include everything.interaction-use-dark-surface;
  @include everything.theme-set-var(text-primary-color, everything.$white);
  @include everything.theme-set-var(background-color, #151b23);
  @include everything.list-set-var(vertical-padding, 0);
  @include everything.list-set-var(item-height, auto);
  @include everything.list-set-var(item-vertical-padding, 0.5rem);
  @include everything.list-set-var(item-horizontal-padding, 0.5rem);
  @include everything.icon-set-var(size, 1rem);
  @include everything.avatar-set-var(size, 1rem);

  :global(.rmd-list-item) {
    @include everything.divider-border-style;

    font-size: 0.75rem;
    height: auto;
    line-height: 1;
  }
}

.title {
  padding: 0.5rem 0.625rem;
}

.autocomplete {
  // remove the outlined border since it is moved to the input
  border: 0;
  padding: 0.625rem;

  // remove the outlined focus behavior since it is moved to the input
  &::after {
    display: none;
  }

  input {
    background: #0d1117;
    border: max(1px, 0.0625rem) solid #3d444db3;
    border-radius: 0.375rem;
    font-size: 0.875rem;
    padding: 0.3125rem 0.75rem;

    &:focus {
      border-color: #1f6feb;
    }
  }
}

// remove the selected option background color
.option {
  background: transparent;
}

.editLabels {
  color: #9198a1;
  gap: 0.5rem;
}

Press Enter to start editing.

Typescript Typing

The Autocomplete option is defined as:

type AutomaticTextExtraction = string | { label: string } | { name: string };
type AutocompleteOption = AutomaticTextExtraction | object;

Each option should extend this type and will automatically be inferred for almost all use cases.

// ✅ Inferred as `string[]`
<Autocomplete {...props} options={["One", "Two", "Three"]} />;

// ✅ Inferred as `string[]`
const options = ["One", "Two", "Three"];
<Autocomplete {...props} options={options} />;

// ✅ Inferred as `("One" | "Two" | "Three")[]`
const options = ["One", "Two", "Three"] as const;
<Autocomplete {...props} options={options} />;

// ✅ Inferred as `{ name: string, value: number }`
const options = [
  { name: "Hello", value: 1 },
  { name: "World", value: 2 },
];
<Autocomplete
  {...props}
  options={options}
/>;

// ✅ Inferred as `State`
interface State {
  name: string;
  abbreviation: string;
}
const options: readonly State[] = [
  { name: "Virginia", abbreviation: "VA" },
  { name: "Whyoming", abbreviation: "WY" },
];
<Autocomplete
  {...props}
  options={options}
/>;

The only time it appears to have an issue is when the list of options are defined with as const:

const options = [
  { fullName: "Virginia", abbreviation: "VA" },
  { fullName: "Whyoming", abbreviation: "WY" },
] as const;
<Autocomplete
  {...props}
  options={options}
  // ❌ Parameter 'state' implicity has an `any` type.
  getOptionLabel={(state) => state.fullName}
/>;

The type issue can be resolved by one of the following:

const options = [
  { fullName: "Virginia", abbreviation: "VA" },
  { fullName: "Whyoming", abbreviation: "WY" },
] as const;

type State = (typeof options)[number];

// 1. Provide the type definition to the `getOptionLabel` parameter
<Autocomplete
  {...props}
  // ✅ `state` and `options` are set to the `State` type
  options={options}
  getOptionLabel={(state: State) => state.fullName}
/>;

// 2. Provide the type definition as the `Autocomplete` type parameter
<Autocomplete<State>
  {...props}
  // ✅ `state` and `options` are set to the `State` type
  options={options}
  getOptionLabel={(state) => state.fullName}
/>;

Accessibility

The Autocomplete component implements the combobox so the <input /> will remain focused while the options gain styles to show they are focused.

Labels

Keyboard Movement

The following keyboard movement has been implemented:

Closed Listbox

Open Listbox