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>
)
}