Skip to content
Vy logo
Components

Autocomplete (New Combobox)

A Autocomplete is a dropdown list you can search in and filter.


This replaces Combobox which which will deprecated eventually.

Examples

A simple autocomplete

Map out a list of items. Pass item to each AutocompleteItem.

() => {
  const countries = [
    { value: "no", label: "Norge" },
    { value: "se", label: "Sverige" },
    { value: "dk", label: "Danmark" },
    { value: "fi", label: "Finland" },
    { value: "de", label: "Tyskland" },
    { value: "fr", label: "Frankrike" },
    { value: "nl", label: "Nederland" },
  ];

  return (
    <Autocomplete label="Velg land">
      {countries.map((item) => (
        <AutocompleteItem item={item} key={item.value}>{item.label}</AutocompleteItem>
      ))}
    </Autocomplete>
  );
};

Controlled autocomplete

Use props value and onValueChange to control combobox value.
Use props inputvalue and onInputValueChange to control input value

() => {
  const countries = [
    { value: "no", label: "Norge" },
    { value: "se", label: "Sverige" },
    { value: "dk", label: "Danmark" },
    { value: "fi", label: "Finland" },
    { value: "de", label: "Tyskland" },
    { value: "fr", label: "Frankrike" },
    { value: "nl", label: "Nederland" },
  ];

  const [value, setValue] = React.useState<string[]>([]);
  const [inputValue, setInputValue] = React.useState("");

  return (
    <Autocomplete
      label="Velg land"
      onValueChange={({ value }) => setValue(value)}
      onInputValueChange={({ inputValue }) => setInputValue(inputValue)}
      inputValue={inputValue}
      variant="floating"
      value={value}
    >
      {countries.map((item) => (
        <AutocompleteItem item={item} key={item.value}>
          {item.label}
        </AutocompleteItem>
      ))}
    </Autocomplete>
  );
}

Multiple items example

Pass multiple to allow multiple items to be selected

() => {
  const countries = [
    { value: "no", label: "Norge" },
    { value: "se", label: "Sverige" },
    { value: "dk", label: "Danmark" },
    { value: "fi", label: "Finland" },
    { value: "de", label: "Tyskland" },
    { value: "fr", label: "Frankrike" },
    { value: "nl", label: "Nederland" },
  ];

  const [selectedCountries, setSelectedCountries] = React.useState([]);

  return (
    <Stack gap="2">
      <Wrap gap="2">
        {selectedCountries.map((country) => (
          <ChoiceChip
            chipType="filter"
            checked
            size="xs"
            key={country.value}
            onClick={() =>
              setSelectedCountries((previous) =>
                previous.filter((c) => c.value !== country.value),
              )
            }
          >
            {country.label}
          </ChoiceChip>
        ))}
      </Wrap>
      <Autocomplete
        label="Velg land"
        onValueChange={({ value }) =>
          setSelectedCountries(
            value.map((v) => countries.find((c) => c.value === v)).filter(Boolean)
          )
        }
        value={selectedCountries.map(({ value }) => value)}
        multiple
      >
        {countries.map((item) => (
          <AutocompleteItem item={item} key={item.value}>
            {item.label}
          </AutocompleteItem>
        ))}
      </Autocomplete>
    </Stack>
  );
};


Variants

Comes with variants core and floating

() => {
  const countries = [
    { value: "no", label: "Norge" },
    { value: "se", label: "Sverige" },
    { value: "dk", label: "Danmark" },
  ];

  return (
   <Flex gap="4">
      <Autocomplete label="Velg land (Core)" variant="core" >
        {countries.map((item) => (
          <AutocompleteItem item={item} key={item.value}>{item.label}</AutocompleteItem>
        ))}
      </Autocomplete>
      <Autocomplete label="Velg land (floating)" variant="floating">
        {countries.map((item) => (
          <AutocompleteItem item={item} key={item.value}>{item.label} </AutocompleteItem>
        ))}
      </Autocomplete>
    </Flex>
  );
};


Rendering custom items and sections

Use AutocompleteItemGroup and AutocompleteItemGroupLabel to group items into sections.

Pass leftIcon to Autocomplete for icon in the input

Pass emptyLabel to customize what is shown when no results are found.

Simply pass whatever you want into ComboboxItem to create custom items.

() => {
  const popularSearches = [
    {
      label: "Skøyen",
      subLabel: "Stasjon i Oslo",
      value: "skoyen",
    },
    {
      label: "Jernbanetorget",
      subLabel: "Oslo sentrum",
      value: "jernbanetorget",
    },
    {
      label: "Drammen",
      subLabel: "Drammen stasjon",
      value: "drammen",
    },
  ];

  const myPosition = {
    label: "Min posisjon",
    value: "min-posisjon",
  };

  return (
    <Autocomplete
      label="Hvor reiser du til?"
      leftIcon={<DestinationOutline24Icon />}
      emptyLabel={
        <Flex color="text.secondary" justifyContent="center" py="4">
          Ingen treff. Prøv å søke etter en stasjon eller adresse. <FrownFill24Icon />
        </Flex>
      }
    >
      <AutocompleteItem item={myPosition} py="2">
        {myPosition.label} <PositionOutline24Icon />
      </AutocompleteItem>
      <AutocompleteItemGroup>
        <AutocompleteItemGroupLabel>Populære søk</AutocompleteItemGroupLabel>
        {popularSearches.map((item) => (
          <AutocompleteItem item={item} key={item.value}>
            <Stack>
              <Text variant="sm">{item.label}</Text>
              <Text variant="xs">{item.subLabel}</Text>
            </Stack>
            <Flex gap="1">
              <SubwayOutline24Icon />
              <TrainOutline24Icon />
            </Flex>
          </AutocompleteItem>
        ))}
      </AutocompleteItemGroup>
    </Autocomplete>
  );
};

Invalid combobox

Pass invalid and errorText to show invalid combobox with error message

() => {
  const countries = [
    { value: "no", label: "Norge" },
    { value: "se", label: "Sverige" },
    { value: "dk", label: "Danmark" },
  ];

  return (
    <Autocomplete label="Velg land" invalid={true} errorText="Feil valg">
      {countries.map((item) => (
        <AutocompleteItem item={item} key={item.value}>{item.label}</AutocompleteItem>
      ))}
    </Autocomplete>
  );
};

Simple example with async data

  • Pass filteredExternally to Autocomplete to not use the internal filtering. Useful for async data filtering to an external API
  • Pass loading for loading state inside the autocomplete dropdown.
() => {
    const useMockDestinationQuery = (searchQuery: string) => {
        return useSwr(
            searchQuery ? [searchQuery, 'destinations'] : null,
            async () => {
                await new Promise((resolve) => setTimeout(resolve, 500))

                const destinations = [
                    { label: 'Oslo S', subLabel: 'Oslo', value: 'oslo-s' },
                    { label: 'Bergen', subLabel: 'Bergen stasjon', value: 'bergen' },
                    { label: 'Trondheim', subLabel: 'Trondheim stasjon', value: 'trondheim' },
                    { label: 'Tromsø', subLabel: 'Tromsø stasjon', value: 'tromso' },
                ]

                return destinations.filter((destination) =>
                    destination.label.toLowerCase().includes(searchQuery.toLowerCase()),
                )
            },
            {
                keepPreviousData: true,
            },
        )
    }

    const [searchQuery, setSearchQuery] = React.useState('')
    const [selectedDestination, setSelectedDestination] = React.useState()

    const { data: destinations, isLoading } = useMockDestinationQuery(searchQuery)

    return (
        <Autocomplete
            label="Hvor reiser du?"
            filteredExternally
            loading={isLoading}
            inputValue={searchQuery}
            value={selectedDestination?.value}
            onInputValueChange={({ inputValue }) => setSearchQuery(inputValue)}
            onValueChange={({ items }) => setSelectedDestination(items[0])}
        >
            {searchQuery && destinations?.map((item) => (
                <AutocompleteItem item={item} key={item.value}>
                    <Stack>
                        <Text variant="sm">{item.label}</Text>
                        <Text variant="xs">{item.subLabel}</Text>
                    </Stack>
                </AutocompleteItem>
            ))}
        </Autocomplete>
    )
}

Complex example with async data

Implemented travel search with controlled autocompletes.

  • Use onFlip callback with flipAriaLabel to add a flip-button between two inputs
  • Pass filteredExternally to Autocomplete to not use the internal filtering. Useful for async data filtering to an external API
  • Pass loading for loading state inside the autocomplete dropdown.
() => {
  const useMockDestinationQuery = (searchQuery: string) => {
    return useSwr(
      [searchQuery, "destinations"],
      async () => {
        await new Promise((resolve) => setTimeout(resolve, 250));
        return [
          { label: "Oslo S", subLabel: "Oslo", value: "oslo-s" },
          { label: "Bergen", subLabel: "Bergen stasjon", value: "bergen" },
          {
            label: "Trondheim",
            subLabel: "Trondheim stasjon",
            value: "trondheim",
          },
          { label: "Tromsø", subLabel: "Tromsø stasjon", value: "tromso" },
        ].filter((destination) =>
          destination.label.toLowerCase().includes(searchQuery.toLowerCase()),
        );
      },
      {
        keepPreviousData: true,
      },
    );
  };

  const [searchQueryFrom, setSearchQueryFrom] = React.useState("");
  const [searchQueryTo, setSearchQueryTo] = React.useState("");

  const [from, setFrom] = React.useState();
  const [to, setTo] = React.useState();

  const { data: destinationsFrom, isLoading: isLoadingFrom } =
    useMockDestinationQuery(searchQueryFrom);

  const { data: destinationsTo, isLoading: isLoadingTo } =
    useMockDestinationQuery(searchQueryTo);

  return (
    <AttachedInputs
      onFlip={() => {
        setFrom(to);
        setTo(from);
        setSearchQueryFrom(searchQueryTo);
        setSearchQueryTo(searchQueryFrom);
      }}
      flipAriaLabel="Bytt avgang og avreise"
      orientation={{ xlDown: "vertical", xl: "horizontal" }}
    >
      <Autocomplete
        label="Hvor reiser du fra?"
        leftIcon={<DestinationOutline24Icon />}
        variant="floating"
        filteredExternally
        loading={isLoadingFrom}
        onInputValueChange={({ inputValue }) => setSearchQueryFrom(inputValue)}
        onValueChange={({ items }) => setFrom(items[0])}
        value={from?.id}
        inputValue={searchQueryFrom}
      >
        <AutocompleteItemGroup>
          <AutocompleteItemGroupLabel>Tidligere søk</AutocompleteItemGroupLabel>
          {destinationsFrom?.map((item) => (
            <AutocompleteItem item={item} key={item.value}>
              <Stack>
                <Text variant="sm">{item.label}</Text>
                <Text variant="xs">{item.subLabel}</Text>
              </Stack>
              <Flex gap="1">
                <SubwayOutline24Icon />
                <TrainOutline24Icon />
              </Flex>
            </AutocompleteItem>
          ))}
        </AutocompleteItemGroup>
      </Autocomplete>
      <Autocomplete
        label="Hvor reiser du til?"
        leftIcon={<DestinationOutline24Icon />}
        variant="floating"
        filteredExternally
        loading={isLoadingTo}
        onInputValueChange={({ inputValue }) => setSearchQueryTo(inputValue)}
        onValueChange={({ items }) => setTo(items[0])}
        value={to?.id}
        inputValue={searchQueryTo}
      >
        <AutocompleteItemGroup>
          <AutocompleteItemGroupLabel>Tidligere søk</AutocompleteItemGroupLabel>
          {destinationsTo?.map((item) => (
            <AutocompleteItem item={item} key={item.value}>
              <Stack>
                <Text variant="sm">{item.label}</Text>
                <Text variant="xs">{item.subLabel}</Text>
              </Stack>
              <Flex gap="1">
                <SubwayOutline24Icon />
                <TrainOutline24Icon />
              </Flex>
            </AutocompleteItem>
          ))}
        </AutocompleteItemGroup>
      </Autocomplete>
    </AttachedInputs>
  );
};

Inside Dialog example

() => {
  const countries = [
    { value: "no", label: "Norge" },
    { value: "se", label: "Sverige" },
    { value: "dk", label: "Danmark" },
    { value: "fi", label: "Finland" },
    { value: "de", label: "Tyskland" },
    { value: "fr", label: "Frankrike" },
    { value: "nl", label: "Nederland" },
  ];

  return (
    <DialogRoot size="md">
      <DialogTrigger asChild>
        <Button size="md">Open autocomplete inside dialog</Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Dialog Title</DialogTitle>
        </DialogHeader>
        <DialogBody>
          <Autocomplete label="Velg land">
            {countries.map((item) => (
              <AutocompleteItem item={item}>{item.label}</AutocompleteItem>
            ))}
          </Autocomplete>
        </DialogBody>
        <DialogFooter>
          <DialogActionTrigger asChild>
            <Button variant="tertiary">Lukk</Button>
          </DialogActionTrigger>
          <DialogActionTrigger asChild>
            <Button variant="primary">Ok</Button>
          </DialogActionTrigger>
        </DialogFooter>
      </DialogContent>
    </DialogRoot>
  );
};