ComponentsAutocomplete

Autocomplete

A text input that offers suggestions while the user types, and keeps whatever they typed.


An autocomplete is not a select: the input's text is the value, and a suggestion is a shortcut to a value the user could have typed. Base UI filters a static items list itself in the default list mode.

Demo

'use client'

import {
  Autocomplete,
  AutocompleteContent,
  AutocompleteInput,
  AutocompleteItem
} from '@immit/ui/autocomplete'

const FRUITS = [
  'Apple',
  'Banana',
  'Blueberry',
  'Cherry',
  'Grapes',
  'Mango',
  'Peach',
  'Pineapple'
]

export function AutocompleteDemo() {
  return (
    <Autocomplete items={FRUITS}>
      <AutocompleteInput placeholder="Search fruit" className="w-64" />
      <AutocompleteContent empty="No fruit matches">
        {(fruit: string) => (
          <AutocompleteItem key={fruit} value={fruit}>
            {fruit}
          </AutocompleteItem>
        )}
      </AutocompleteContent>
    </Autocomplete>
  )
}

Groups from a server

With mode="none" the list is whatever items holds, so a server or a custom matcher decides what shows. An item's onClick fires on a pointer press and on Enter over a highlighted item, and after it Base UI writes the item's label into the input with the reason item-press, so a box that clears itself on a pick ignores that reason in onValueChange. Base UI fixes open as controlled or uncontrolled on the first render, so a box that shuts the popup when the list is empty controls open for its whole life.

Nothing picked

'use client'

import { useState } from 'react'
import {
  Autocomplete,
  AutocompleteCollection,
  AutocompleteContent,
  AutocompleteGroup,
  AutocompleteGroupLabel,
  AutocompleteInput,
  AutocompleteItem
} from '@immit/ui/autocomplete'
import { Building2, MapPin } from 'lucide-react'

type Suggestion = { kind: 'company' | 'place'; value: string }

type Group = { value: string; items: Suggestion[] }

const BOARD: Group[] = [
  {
    value: 'Companies',
    items: ['Stripe', 'Datadog', 'Mercari'].map((value) => ({
      kind: 'company' as const,
      value
    }))
  },
  {
    value: 'Places',
    items: ['Tokyo', 'Osaka', 'Fukuoka'].map((value) => ({
      kind: 'place' as const,
      value
    }))
  }
]

function matching(query: string): Group[] {
  const needle = query.trim().toLowerCase()
  return BOARD.map((group) => ({
    ...group,
    items: group.items.filter((item) =>
      item.value.toLowerCase().startsWith(needle)
    )
  })).filter((group) => group.items.length > 0)
}

export function AutocompleteWithGroups() {
  const [query, setQuery] = useState('')
  const [open, setOpen] = useState(false)
  const [picked, setPicked] = useState<Suggestion | null>(null)
  const groups = matching(query)

  return (
    <div className="space-y-3">
      <Autocomplete
        items={groups}
        mode="none"
        value={query}
        onValueChange={(next, details) => {
          if (details.reason !== 'item-press') {
            setQuery(next)
          }
        }}
        open={open && groups.length > 0}
        onOpenChange={setOpen}
        openOnInputClick
        itemToStringValue={(item: Suggestion) => item.value}
      >
        <AutocompleteInput placeholder="Company or place" className="w-72" />
        <AutocompleteContent>
          {(group: Group) => (
            <AutocompleteGroup key={group.value} items={group.items}>
              <AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
              <AutocompleteCollection>
                {(item: Suggestion) => (
                  <AutocompleteItem
                    key={`${item.kind}:${item.value}`}
                    value={item}
                    onClick={() => {
                      setPicked(item)
                      setQuery('')
                      setOpen(false)
                    }}
                  >
                    {item.kind === 'company' ? (
                      <Building2 className="text-icon size-4" />
                    ) : (
                      <MapPin className="text-icon size-4" />
                    )}
                    {item.value}
                  </AutocompleteItem>
                )}
              </AutocompleteCollection>
            </AutocompleteGroup>
          )}
        </AutocompleteContent>
      </Autocomplete>
      <p className="text-muted-foreground text-sm">
        {picked ? `Picked ${picked.kind}: ${picked.value}` : 'Nothing picked'}
      </p>
    </div>
  )
}