An autocomplete combines a select with filtering, allowing users to search and select from a list of options.
Import
import { Autocomplete, useFilter } from "heroui-solid";Usage
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
Tag,
TagGroup,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function Default() {
const { contains } = useFilter({ sensitivity: "base" })
const [selectedKeys, setSelectedKeys] = createSignal<string[]>([])
const items = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
const onRemoveTags = (keys: Set<string>) => {
setSelectedKeys((prev) => prev.filter((key) => !keys.has(key)))
}
return (
<Autocomplete
class="w-[256px]"
placeholder="Select states"
selectionMode="multiple"
value={selectedKeys()}
onChange={(keys) => setSelectedKeys(keys as string[])}
>
<Label>States to Visit</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
return (
<TagGroup size="sm" onRemove={onRemoveTags}>
<TagGroup.List>
<For each={state.selectedItems.map((item) => item.key)}>
{(selectedItemKey) => {
const item = items.find((s) => s.id === selectedItemKey)
if (!item) return null
return <Tag id={item.id}>{item.name}</Tag>
}}
</For>
</TagGroup.List>
</TagGroup>
)
}}
</Autocomplete.Value>
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Anatomy
Import the Autocomplete component and access all parts using dot notation.
import { Autocomplete, Label, Description, SearchField, ListBox } from "heroui-solid";
export default () => (
<Autocomplete>
<Label />
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Description />
<Autocomplete.Popover>
<Autocomplete.Filter>
<SearchField>
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input />
</SearchField.Group>
</SearchField>
<ListBox>
<ListBox.Item>
<Label />
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
);With Description
import {
Autocomplete,
Description,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function WithDescription() {
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
return (
<Autocomplete
class="w-[256px]"
placeholder="Select one"
selectionMode="single"
value={selectedKey()}
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>State</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
<Description>Select your state of residence</Description>
</Autocomplete>
)
}Multiple Select
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
Tag,
TagGroup,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function MultipleSelect() {
const [selectedKeys, setSelectedKeys] = createSignal<string[]>([])
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "florida", name: "Florida" },
{ id: "new-york", name: "New York" },
{ id: "illinois", name: "Illinois" },
{ id: "pennsylvania", name: "Pennsylvania" }
]
const onRemoveTags = (keys: Set<string>) => {
setSelectedKeys((prev) => prev.filter((key) => !keys.has(key)))
}
return (
<Autocomplete
class="w-[256px]"
placeholder="Select states"
selectionMode="multiple"
value={selectedKeys()}
onChange={(keys) => setSelectedKeys(keys as string[])}
>
<Label>States</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
return (
<TagGroup size="sm" onRemove={onRemoveTags}>
<TagGroup.List>
<For each={state.selectedItems.map((item) => item.key)}>
{(selectedItemKey) => {
const item = items.find((s) => s.id === selectedItemKey)
if (!item) return null
return <Tag id={item.id}>{item.name}</Tag>
}}
</For>
</TagGroup.List>
</TagGroup>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}With Sections
import {
Autocomplete,
EmptyState,
Header,
Label,
ListBox,
SearchField,
Separator,
useFilter
} from "heroui-solid"
import { createSignal } from "solid-js"
export function WithSections() {
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
return (
<Autocomplete
class="w-[256px]"
placeholder="Select a country"
selectionMode="single"
value={selectedKey()}
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>Country</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search countries..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<ListBox.Section>
<Header>North America</Header>
<ListBox.Item id="usa" textValue="United States">
United States
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="canada" textValue="Canada">
Canada
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="mexico" textValue="Mexico">
Mexico
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Europe</Header>
<ListBox.Item id="uk" textValue="United Kingdom">
United Kingdom
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="france" textValue="France">
France
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="germany" textValue="Germany">
Germany
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="spain" textValue="Spain">
Spain
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="italy" textValue="Italy">
Italy
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox.Section>
<Separator />
<ListBox.Section>
<Header>Asia</Header>
<ListBox.Item id="japan" textValue="Japan">
Japan
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="china" textValue="China">
China
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="india" textValue="India">
India
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="south-korea" textValue="South Korea">
South Korea
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox.Section>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}With Disabled Options
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { createSignal } from "solid-js"
export function WithDisabledOptions() {
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
return (
<Autocomplete
class="w-[256px]"
disabledKeys={["cat", "kangaroo"]}
placeholder="Select an animal"
selectionMode="single"
value={selectedKey()}
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>Animal</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search animals..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<ListBox.Item id="dog" textValue="Dog">
Dog
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="cat" textValue="Cat">
Cat
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="bird" textValue="Bird">
Bird
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="kangaroo" textValue="Kangaroo">
Kangaroo
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="elephant" textValue="Elephant">
Elephant
<ListBox.ItemIndicator />
</ListBox.Item>
<ListBox.Item id="tiger" textValue="Tiger">
Tiger
<ListBox.ItemIndicator />
</ListBox.Item>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Allows Empty Collection
The allowsEmptyCollection prop enables the autocomplete to function even when there are no items in the collection. This is useful for scenarios where the list might be empty initially or when all items are filtered out.
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
export function AllowsEmptyCollection() {
const { contains } = useFilter({ sensitivity: "base" })
return (
<Autocomplete
allowsEmptyCollection
class="w-[256px]"
placeholder="Select one"
selectionMode="single"
>
<Label>State</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
/>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Custom Indicator
import { ChevronsExpandVertical } from "gravity-icons-solid"
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function CustomIndicator() {
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
return (
<Autocomplete
class="w-[256px]"
placeholder="Select one"
selectionMode="single"
value={selectedKey()}
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>State</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator class="size-3">
<ChevronsExpandVertical />
</Autocomplete.Indicator>
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Required
import {
Autocomplete,
Button,
EmptyState,
FieldError,
Form,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { For } from "solid-js"
export function Required() {
const onSubmit = (e: SubmitEvent) => {
e.preventDefault()
alert("Form submitted successfully!")
}
const { contains } = useFilter({ sensitivity: "base" })
const states = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
const countries = [
{ id: "usa", name: "United States" },
{ id: "canada", name: "Canada" },
{ id: "mexico", name: "Mexico" },
{ id: "uk", name: "United Kingdom" },
{ id: "france", name: "France" },
{ id: "germany", name: "Germany" }
]
return (
<Form class="flex w-[256px] flex-col gap-4" onSubmit={onSubmit}>
<Autocomplete
isRequired
class="w-full"
name="state"
placeholder="Select one"
selectionMode="single"
>
<Label>State</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={states}>
{(state) => (
<ListBox.Item id={state.id} textValue={state.name}>
{state.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
<FieldError />
</Autocomplete>
<Autocomplete
isRequired
class="w-full"
name="country"
placeholder="Select a country"
selectionMode="single"
>
<Label>Country</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search countries..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={countries}>
{(country) => (
<ListBox.Item id={country.id} textValue={country.name}>
{country.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
<FieldError />
</Autocomplete>
<Button type="submit">Submit</Button>
</Form>
)
}Full Width
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
Surface,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function FullWidth() {
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
return (
<Surface class="w-[380px] space-y-4 rounded-3xl p-6">
<Autocomplete
fullWidth
placeholder="Select one"
selectionMode="single"
value={selectedKey()}
variant="secondary"
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>State</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
</Surface>
)
}Variants
The Autocomplete component supports two visual variants:
primary(default) - Standard styling with shadow, suitable for most use casessecondary- Lower emphasis variant without shadow, suitable for use in Surface components
Single Select Variants
Multiple Select Variants
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
Tag,
TagGroup,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function Variants() {
const [selectedKey1, setSelectedKey1] = createSignal<string | null>(null)
const [selectedKey2, setSelectedKey2] = createSignal<string | null>(null)
const [selectedKeys1, setSelectedKeys1] = createSignal<string[]>([])
const [selectedKeys2, setSelectedKeys2] = createSignal<string[]>([])
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "option1", name: "Option 1" },
{ id: "option2", name: "Option 2" },
{ id: "option3", name: "Option 3" },
{ id: "option4", name: "Option 4" }
]
const onRemoveTags1 = (keys: Set<string>) => {
setSelectedKeys1((prev) => prev.filter((key) => !keys.has(key)))
}
const onRemoveTags2 = (keys: Set<string>) => {
setSelectedKeys2((prev) => prev.filter((key) => !keys.has(key)))
}
return (
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<h3 class="text-lg font-semibold">Single Select Variants</h3>
<div class="flex flex-col gap-4">
<Autocomplete
class="w-[256px]"
placeholder="Select one"
selectionMode="single"
value={selectedKey1()}
variant="primary"
onChange={(key) => setSelectedKey1(key as string | null)}
>
<Label>Primary variant</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => (
<EmptyState>No results found</EmptyState>
)}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
<Autocomplete
class="w-[256px]"
placeholder="Select one"
selectionMode="single"
value={selectedKey2()}
variant="secondary"
onChange={(key) => setSelectedKey2(key as string | null)}
>
<Label>Secondary variant</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => (
<EmptyState>No results found</EmptyState>
)}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
</div>
</div>
<div class="flex flex-col gap-4">
<h3 class="text-lg font-semibold">Multiple Select Variants</h3>
<div class="flex flex-col gap-4">
<Autocomplete
class="w-[256px]"
placeholder="Select multiple"
selectionMode="multiple"
value={selectedKeys1()}
variant="primary"
onChange={(keys) => setSelectedKeys1(keys as string[])}
>
<Label>Primary variant</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
return (
<TagGroup size="sm" onRemove={onRemoveTags1}>
<TagGroup.List>
<For each={state.selectedItems.map((item) => item.key)}>
{(selectedItemKey) => {
const item = items.find(
(s) => s.id === selectedItemKey
)
if (!item) return null
return <Tag id={item.id}>{item.name}</Tag>
}}
</For>
</TagGroup.List>
</TagGroup>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => (
<EmptyState>No results found</EmptyState>
)}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
<Autocomplete
class="w-[256px]"
placeholder="Select multiple"
selectionMode="multiple"
value={selectedKeys2()}
variant="secondary"
onChange={(keys) => setSelectedKeys2(keys as string[])}
>
<Label>Secondary variant</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
return (
<TagGroup
size="sm"
variant="surface"
onRemove={onRemoveTags2}
>
<TagGroup.List>
<For each={state.selectedItems.map((item) => item.key)}>
{(selectedItemKey) => {
const item = items.find(
(s) => s.id === selectedItemKey
)
if (!item) return null
return <Tag id={item.id}>{item.name}</Tag>
}}
</For>
</TagGroup.List>
</TagGroup>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => (
<EmptyState>No results found</EmptyState>
)}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
</div>
</div>
</div>
)
}In Surface
When used inside a Surface component, use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
Surface,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function FullWidth() {
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
return (
<Surface class="w-[380px] space-y-4 rounded-3xl p-6">
<Autocomplete
fullWidth
placeholder="Select one"
selectionMode="single"
value={selectedKey()}
variant="secondary"
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>State</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
</Surface>
)
}Custom Value
You can customize the displayed value using render props:
import {
Autocomplete,
Avatar,
AvatarFallback,
AvatarImage,
Description,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function UserSelection() {
const users = [
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg",
email: "bob@heroui.com",
fallback: "B",
id: "1",
name: "Bob"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg",
email: "fred@heroui.com",
fallback: "F",
id: "2",
name: "Fred"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg",
email: "martha@heroui.com",
fallback: "M",
id: "3",
name: "Martha"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg",
email: "john@heroui.com",
fallback: "J",
id: "4",
name: "John"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg",
email: "jane@heroui.com",
fallback: "J",
id: "5",
name: "Jane"
}
]
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
return (
<Autocomplete
class="w-[256px]"
placeholder="Select a user"
selectionMode="single"
value={selectedKey()}
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>User</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
const selectedItems = state.selectedItems
if (selectedItems.length > 1) {
return `${selectedItems.length} users selected`
}
const selectedItem = users.find(
(user) => user.id === selectedItems[0]?.key
)
if (!selectedItem) {
return defaultChildren
}
return (
<div class="flex items-center gap-2">
<Avatar class="size-4" size="sm">
<AvatarImage src={selectedItem.avatarUrl} />
<AvatarFallback>{selectedItem.fallback}</AvatarFallback>
</Avatar>
<span>{selectedItem.name}</span>
</div>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search users..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={users}>
{(user) => (
<ListBox.Item id={user.id} textValue={user.name}>
<Avatar size="sm">
<AvatarImage src={user.avatarUrl} />
<AvatarFallback>{user.fallback}</AvatarFallback>
</Avatar>
<div class="flex flex-col">
<Label>{user.name}</Label>
<Description>{user.email}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Controlled
Selected: California
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function Controlled() {
const states = [
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "florida", name: "Florida" },
{ id: "new-york", name: "New York" },
{ id: "illinois", name: "Illinois" },
{ id: "pennsylvania", name: "Pennsylvania" }
]
const [state, setState] = createSignal<string | null>("california")
const { contains } = useFilter({ sensitivity: "base" })
const selectedState = () => states.find((s) => s.id === state())
return (
<div class="space-y-2">
<Autocomplete
class="w-[256px]"
placeholder="Select a state"
selectionMode="single"
value={state()}
onChange={(key) => setState(key as string | null)}
>
<Label>State (controlled)</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={states}>
{(state) => (
<ListBox.Item id={state.id} textValue={state.name}>
{state.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
<p class="text-sm text-muted">
Selected: {selectedState()?.name || "None"}
</p>
</div>
)
}Controlled Multiple
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
Tag,
TagGroup,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function MultipleSelect() {
const [selectedKeys, setSelectedKeys] = createSignal<string[]>([])
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "florida", name: "Florida" },
{ id: "new-york", name: "New York" },
{ id: "illinois", name: "Illinois" },
{ id: "pennsylvania", name: "Pennsylvania" }
]
const onRemoveTags = (keys: Set<string>) => {
setSelectedKeys((prev) => prev.filter((key) => !keys.has(key)))
}
return (
<Autocomplete
class="w-[256px]"
placeholder="Select states"
selectionMode="multiple"
value={selectedKeys()}
onChange={(keys) => setSelectedKeys(keys as string[])}
>
<Label>States</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
return (
<TagGroup size="sm" onRemove={onRemoveTags}>
<TagGroup.List>
<For each={state.selectedItems.map((item) => item.key)}>
{(selectedItemKey) => {
const item = items.find((s) => s.id === selectedItemKey)
if (!item) return null
return <Tag id={item.id}>{item.name}</Tag>
}}
</For>
</TagGroup.List>
</TagGroup>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Controlled Open State
Autocomplete is closed
import {
Autocomplete,
Button,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function ControlledOpenState() {
const [isOpen, setIsOpen] = createSignal(false)
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
return (
<div class="space-y-4">
<Autocomplete
class="w-[256px]"
isOpen={isOpen()}
placeholder="Select one"
selectionMode="single"
onOpenChange={setIsOpen}
>
<Label>State</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
<Button onClick={() => setIsOpen(!isOpen())}>
{isOpen() ? "Close" : "Open"} Autocomplete
</Button>
<p class="text-sm text-muted">
Autocomplete is {isOpen() ? "open" : "closed"}
</p>
</div>
)
}Asynchronous Filtering
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
Spinner
} from "heroui-solid"
import { createEffect, createResource, createSignal, onCleanup } from "solid-js"
import { isServer } from "solid-js/web"
import { cn } from "tailwind-variants"
interface Character {
name: string
}
export function AsynchronousFiltering() {
const [filterText, setFilterText] = createSignal("")
const [debouncedText, setDebouncedText] = createSignal("")
createEffect(() => {
const text = filterText()
const timer = setTimeout(() => setDebouncedText(text), 300)
onCleanup(() => clearTimeout(timer))
})
const [characters] = createResource(debouncedText, async (search) => {
if (isServer) return []
try {
const res = await fetch(
`https://swapi.py4e.com/api/people/?search=${search}`
)
const json = await res.json()
return (json.results ?? []) as Character[]
} catch {
return []
}
})
return (
<Autocomplete
allowsEmptyCollection
class="w-[256px]"
placeholder="Search..."
selectionMode="single"
>
<Label>Search a Star Wars characters</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter
inputValue={filterText()}
onInputChange={setFilterText}
>
<SearchField
autoFocus
class="sticky top-0 z-10"
name="search"
variant="secondary"
>
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search characters..." />
<Spinner
size="sm"
class={cn("absolute top-1/2 right-2 -translate-y-1/2", {
"pointer-events-none opacity-0": !characters.loading
})}
/>
<SearchField.ClearButton
class={cn({
"pointer-events-none opacity-0": !!characters.loading
})}
/>
</SearchField.Group>
</SearchField>
<ListBox
class="max-h-[420px] overflow-y-auto"
items={characters() ?? []}
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
{(item: Character) => (
<ListBox.Item id={item.name} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Virtualization
Autocomplete supports virtualization through Virtualizer, enabling efficient rendering of large datasets by displaying only the rows visible within the viewport.
import {
Autocomplete,
Description,
EmptyState,
Label,
ListBox,
ListLayout,
SearchField,
useFilter,
Virtualizer
} from "heroui-solid"
import { createMemo, createSignal } from "solid-js"
interface User {
email: string
id: number
name: string
}
function generateUsers(n: number): User[] {
const firstNames = [
"Emma",
"Liam",
"Olivia",
"Noah",
"Ava",
"James",
"Sophia",
"Oliver",
"Isabella",
"Lucas",
"Mia",
"Ethan",
"Charlotte",
"Mason",
"Amelia",
"Logan",
"Harper",
"Alexander",
"Ella",
"Benjamin"
]
const lastNames = [
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Rodriguez",
"Martinez",
"Anderson",
"Taylor",
"Thomas",
"Jackson",
"White",
"Harris",
"Clark",
"Lewis",
"Robinson",
"Walker"
]
const users: User[] = []
for (let i = 0; i < n; i++) {
const firstName = firstNames[i % firstNames.length]
const lastName =
lastNames[Math.floor(i / firstNames.length) % lastNames.length]
const name = `${firstName} ${lastName}`
users.push({
email: `${firstName?.toLowerCase()}.${lastName?.toLowerCase()}@acme.com`,
id: i + 1,
name
})
}
return users
}
export function Virtualization() {
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const [searchQuery, setSearchQuery] = createSignal("")
const { contains } = useFilter({ sensitivity: "base" })
const allUsers = generateUsers(1000)
const filteredUsers = createMemo(() => {
const query = searchQuery()
if (!query) return allUsers
return allUsers.filter(
(user) => contains(user.name, query) || contains(user.email, query)
)
})
return (
<Autocomplete
allowsEmptyCollection
class="w-[300px]"
placeholder="Select a user"
selectionMode="single"
value={selectedKey()}
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>User</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter
inputValue={searchQuery()}
onInputChange={setSearchQuery}
>
<SearchField
autoFocus
class="sticky top-0 z-10"
name="search"
variant="secondary"
>
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search users..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<Virtualizer layout={ListLayout} layoutOptions={{ rowHeight: 50 }}>
<ListBox
items={filteredUsers()}
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
{(user: User) => (
<ListBox.Item id={String(user.id)} textValue={user.name}>
<div class="flex flex-col">
<Label>{user.name}</Label>
<Description>{user.email}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</ListBox>
</Virtualizer>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Disabled
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { For } from "solid-js"
export function Disabled() {
const { contains } = useFilter({ sensitivity: "base" })
const items = [
{ id: "florida", name: "Florida" },
{ id: "delaware", name: "Delaware" },
{ id: "california", name: "California" },
{ id: "texas", name: "Texas" },
{ id: "new-york", name: "New York" },
{ id: "washington", name: "Washington" }
]
const countries = [
{ id: "argentina", name: "Argentina" },
{ id: "venezuela", name: "Venezuela" },
{ id: "japan", name: "Japan" },
{ id: "france", name: "France" },
{ id: "italy", name: "Italy" },
{ id: "spain", name: "Spain" }
]
return (
<div class="flex flex-col gap-4">
<Autocomplete
isDisabled
class="w-[256px]"
defaultValue="california"
placeholder="Select one"
selectionMode="single"
>
<Label>State</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search states..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={items}>
{(item) => (
<ListBox.Item id={item.id} textValue={item.name}>
{item.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
<Autocomplete
isDisabled
class="w-[256px]"
defaultValue={["argentina", "japan", "france"]}
placeholder="Select countries"
selectionMode="multiple"
>
<Label>Countries to Visit</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search countries..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={countries}>
{(country) => (
<ListBox.Item id={country.id} textValue={country.name}>
{country.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
</div>
)
}Advanced Examples
User Selection
import {
Autocomplete,
Avatar,
AvatarFallback,
AvatarImage,
Description,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function UserSelection() {
const users = [
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg",
email: "bob@heroui.com",
fallback: "B",
id: "1",
name: "Bob"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg",
email: "fred@heroui.com",
fallback: "F",
id: "2",
name: "Fred"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg",
email: "martha@heroui.com",
fallback: "M",
id: "3",
name: "Martha"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg",
email: "john@heroui.com",
fallback: "J",
id: "4",
name: "John"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg",
email: "jane@heroui.com",
fallback: "J",
id: "5",
name: "Jane"
}
]
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
return (
<Autocomplete
class="w-[256px]"
placeholder="Select a user"
selectionMode="single"
value={selectedKey()}
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>User</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
const selectedItems = state.selectedItems
if (selectedItems.length > 1) {
return `${selectedItems.length} users selected`
}
const selectedItem = users.find(
(user) => user.id === selectedItems[0]?.key
)
if (!selectedItem) {
return defaultChildren
}
return (
<div class="flex items-center gap-2">
<Avatar class="size-4" size="sm">
<AvatarImage src={selectedItem.avatarUrl} />
<AvatarFallback>{selectedItem.fallback}</AvatarFallback>
</Avatar>
<span>{selectedItem.name}</span>
</div>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search users..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={users}>
{(user) => (
<ListBox.Item id={user.id} textValue={user.name}>
<Avatar size="sm">
<AvatarImage src={user.avatarUrl} />
<AvatarFallback>{user.fallback}</AvatarFallback>
</Avatar>
<div class="flex flex-col">
<Label>{user.name}</Label>
<Description>{user.email}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}User Selection Multiple
import {
Autocomplete,
Avatar,
AvatarFallback,
AvatarImage,
Description,
EmptyState,
Label,
ListBox,
SearchField,
Tag,
TagGroup,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function UserSelectionMultiple() {
const users = [
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg",
email: "bob@heroui.com",
fallback: "B",
id: "1",
name: "Bob"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg",
email: "fred@heroui.com",
fallback: "F",
id: "2",
name: "Fred"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg",
email: "martha@heroui.com",
fallback: "M",
id: "3",
name: "Martha"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg",
email: "john@heroui.com",
fallback: "J",
id: "4",
name: "John"
},
{
avatarUrl:
"https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg",
email: "jane@heroui.com",
fallback: "J",
id: "5",
name: "Jane"
}
]
const [selectedKeys, setSelectedKeys] = createSignal<string[]>([])
const { contains } = useFilter({ sensitivity: "base" })
const onRemoveTags = (keys: Set<string>) => {
setSelectedKeys((prev) => prev.filter((key) => !keys.has(key)))
}
return (
<Autocomplete
class="w-[256px]"
defaultValue={["1", "2"]}
placeholder="Select your teammates"
selectionMode="multiple"
value={selectedKeys()}
onChange={(keys) => setSelectedKeys(keys as string[])}
>
<Label>Users</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
return (
<TagGroup size="sm" onRemove={onRemoveTags}>
<TagGroup.List>
<For each={state.selectedItems.map((item) => item.key)}>
{(selectedItemKey) => {
const selectedItem = users.find(
(user) => user.id === selectedItemKey
)
if (!selectedItem) {
return null
}
return (
<Tag id={selectedItem.id}>
<Avatar class="size-4" size="sm">
<AvatarImage src={selectedItem.avatarUrl} />
<AvatarFallback>
{selectedItem.fallback}
</AvatarFallback>
</Avatar>
<span>{selectedItem.name}</span>
</Tag>
)
}}
</For>
</TagGroup.List>
</TagGroup>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search users..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No results found</EmptyState>}
>
<For each={users}>
{(user) => (
<ListBox.Item id={user.id} textValue={user.name}>
<Avatar size="sm">
<AvatarImage src={user.avatarUrl} />
<AvatarFallback>{user.fallback}</AvatarFallback>
</Avatar>
<div class="flex flex-col">
<Label>{user.name}</Label>
<Description>{user.email}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Location Search
import {
Autocomplete,
Description,
EmptyState,
Label,
ListBox,
SearchField,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
interface City {
name: string
country: string
}
export function LocationSearch() {
const allCities: City[] = [
{ country: "USA", name: "New York" },
{ country: "USA", name: "Los Angeles" },
{ country: "USA", name: "Chicago" },
{ country: "UK", name: "London" },
{ country: "France", name: "Paris" },
{ country: "Japan", name: "Tokyo" },
{ country: "Australia", name: "Sydney" },
{ country: "Canada", name: "Toronto" },
{ country: "Germany", name: "Berlin" },
{ country: "Spain", name: "Madrid" }
]
const [selectedKey, setSelectedKey] = createSignal<string | null>(null)
const [isLoading, setIsLoading] = createSignal(false)
const { contains } = useFilter({ sensitivity: "base" })
// Simulate async filtering
const customFilter = (text: string, inputValue: string) => {
if (!inputValue) return true
setIsLoading(true)
setTimeout(() => setIsLoading(false), 300)
return contains(text, inputValue)
}
return (
<Autocomplete
class="w-[256px]"
placeholder="Search for a city"
selectionMode="single"
value={selectedKey()}
onChange={(key) => setSelectedKey(key as string | null)}
>
<Label>City</Label>
<Autocomplete.Trigger>
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={customFilter}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search cities..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => (
<EmptyState>
{isLoading() ? "Searching..." : "No cities found"}
</EmptyState>
)}
>
<For each={allCities}>
{(city) => (
<ListBox.Item id={city.name} textValue={city.name}>
<div class="flex flex-col">
<Label>{city.name}</Label>
<Description>{city.country}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Tag Group Selection
import {
Autocomplete,
EmptyState,
Label,
ListBox,
SearchField,
Tag,
TagGroup,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function TagGroupSelection() {
const tags = [
{ id: "react", name: "React" },
{ id: "typescript", name: "TypeScript" },
{ id: "javascript", name: "JavaScript" },
{ id: "nodejs", name: "Node.js" },
{ id: "python", name: "Python" },
{ id: "vue", name: "Vue" },
{ id: "angular", name: "Angular" },
{ id: "nextjs", name: "Next.js" }
]
const [selectedKeys, setSelectedKeys] = createSignal<string[]>([])
const { contains } = useFilter({ sensitivity: "base" })
const onRemoveTags = (keys: Set<string>) => {
setSelectedKeys((prev) => prev.filter((key) => !keys.has(key)))
}
return (
<Autocomplete
class="w-[256px]"
placeholder="Select tags"
selectionMode="multiple"
value={selectedKeys()}
onChange={(keys) => setSelectedKeys(keys as string[])}
>
<Label>Tags</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
return (
<TagGroup size="sm" onRemove={onRemoveTags}>
<TagGroup.List>
<For each={state.selectedItems.map((item) => item.key)}>
{(selectedItemKey) => {
const tag = tags.find((t) => t.id === selectedItemKey)
if (!tag) return null
return <Tag id={tag.id}>{tag.name}</Tag>
}}
</For>
</TagGroup.List>
</TagGroup>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search tags..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => <EmptyState>No tags found</EmptyState>}
>
<For each={tags}>
{(tag) => (
<ListBox.Item id={tag.id} textValue={tag.name}>
{tag.name}
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Email Recipients
import {
Autocomplete,
Description,
EmptyState,
Label,
ListBox,
SearchField,
Tag,
TagGroup,
useFilter
} from "heroui-solid"
import { createSignal, For } from "solid-js"
export function EmailRecipients() {
const emails = [
{
email: "alice@example.com",
id: "alice@example.com",
name: "Alice Johnson"
},
{ email: "bob@example.com", id: "bob@example.com", name: "Bob Smith" },
{
email: "charlie@example.com",
id: "charlie@example.com",
name: "Charlie Brown"
},
{
email: "diana@example.com",
id: "diana@example.com",
name: "Diana Prince"
},
{ email: "eve@example.com", id: "eve@example.com", name: "Eve Wilson" }
]
const [selectedKeys, setSelectedKeys] = createSignal<string[]>([])
const { contains } = useFilter({ sensitivity: "base" })
const onRemoveTags = (keys: Set<string>) => {
setSelectedKeys((prev) => prev.filter((key) => !keys.has(key)))
}
return (
<Autocomplete
class="w-[256px]"
placeholder="Add recipients"
selectionMode="multiple"
value={selectedKeys()}
onChange={(keys) => setSelectedKeys(keys as string[])}
>
<Label>To</Label>
<Autocomplete.Trigger>
<Autocomplete.Value>
{({ defaultChildren, isPlaceholder, state }) => {
if (isPlaceholder || state.selectedItems.length === 0) {
return defaultChildren
}
return (
<TagGroup size="sm" onRemove={onRemoveTags}>
<TagGroup.List>
<For each={state.selectedItems.map((item) => item.key)}>
{(selectedItemKey) => {
const email = emails.find((e) => e.id === selectedItemKey)
if (!email) return null
return <Tag id={email.id}>{email.email}</Tag>
}}
</For>
</TagGroup.List>
</TagGroup>
)
}}
</Autocomplete.Value>
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter filter={contains}>
<SearchField autoFocus name="search" variant="secondary">
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search emails..." />
<SearchField.ClearButton />
</SearchField.Group>
</SearchField>
<ListBox
renderEmptyState={() => (
<EmptyState>No recipients found</EmptyState>
)}
>
<For each={emails}>
{(email) => (
<ListBox.Item id={email.id} textValue={email.email}>
<div class="flex flex-col">
<Label>{email.name}</Label>
<Description>{email.email}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
)}
</For>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}Styling
Passing Tailwind CSS classes
import { Autocomplete, SearchField, ListBox } from "heroui-solid";
function CustomAutocomplete() {
return (
<Autocomplete class="w-full">
<Label>State</Label>
<Autocomplete.Trigger class="rounded-lg border bg-surface p-2">
<Autocomplete.Value />
<Autocomplete.ClearButton />
<Autocomplete.Indicator />
</Autocomplete.Trigger>
<Autocomplete.Popover>
<Autocomplete.Filter>
<SearchField>
<SearchField.Group>
<SearchField.SearchIcon />
<SearchField.Input placeholder="Search..." />
</SearchField.Group>
</SearchField>
<ListBox>
<ListBox.Item id="1" textValue="Item 1" class="hover:bg-surface-secondary">
Item 1
</ListBox.Item>
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
);
}Customizing the component classes
To customize the Autocomplete component classes, you can use the @layer components directive.
@layer components {
.autocomplete {
@apply flex flex-col gap-1;
}
.autocomplete__trigger {
@apply rounded-lg border border-border bg-surface p-2;
}
.autocomplete__value {
@apply text-current;
}
.autocomplete__clear-button {
@apply text-muted hover:text-foreground;
}
.autocomplete__indicator {
@apply text-muted;
}
.autocomplete__popover {
@apply rounded-lg border border-border bg-surface p-2;
}
.autocomplete__popover-dialog {
@apply outline-none;
}
}HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
The Autocomplete component uses these CSS classes (View source styles):
Base Classes
.autocomplete- Base autocomplete container.autocomplete__trigger- The button that triggers the autocomplete.autocomplete__value- The displayed value or placeholder.autocomplete__clear-button- The clear button that removes the selected value.autocomplete__indicator- The dropdown indicator icon.autocomplete__popover- The popover container.autocomplete__popover-dialog- Internal dialog wrapper inside the popover for focus management (matches Popover behavior).autocomplete__filter- The filter wrapper
Variant Classes
.autocomplete--primary- Primary variant with shadow (default).autocomplete--secondary- Secondary variant without shadow, suitable for use in surfaces
State Classes
.autocomplete[data-invalid="true"]- Invalid state.autocomplete__trigger[data-focus-visible="true"]- Focused trigger state.autocomplete__trigger[data-disabled="true"]- Disabled trigger state.autocomplete__value[data-placeholder="true"]- Placeholder state.autocomplete__clear-button[data-empty="true"]- Clear button hidden when no selection.autocomplete__indicator[data-open="true"]- Open indicator state
Interactive States
The component supports both CSS pseudo-classes and data attributes for flexibility:
- Hover:
:hoveror[data-hovered="true"]on trigger - Focus:
:focus-visibleor[data-focus-visible="true"]on trigger - Disabled:
:disabledor[data-disabled="true"]on autocomplete - Open:
[data-open="true"]on indicator
API Reference
Autocomplete Props
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder | string | 'Select an item' | Temporary text that occupies the autocomplete when it is empty |
selectionMode | "single" | "multiple" | "single" | Whether single or multiple selection is enabled |
allowsEmptyCollection | boolean | false | Whether the autocomplete allows an empty collection. When true, the autocomplete can function even with no items. |
isOpen | boolean | - | Sets the open state of the popover (controlled) |
defaultOpen | boolean | - | Sets the default open state of the popover (uncontrolled) |
onOpenChange | (isOpen: boolean) => void | - | Handler called when the open state changes |
disabledKeys | Iterable<string> | - | Keys of disabled items |
isDisabled | boolean | - | Whether the autocomplete is disabled |
value | string | string[] | null | - | Current value (controlled) |
defaultValue | string | string[] | null | - | Default value (uncontrolled) |
onChange | (value: string | string[] | null) => void | - | Handler called when the value changes |
isRequired | boolean | - | Whether user input is required |
isInvalid | boolean | - | Whether the autocomplete value is invalid |
name | string | - | The name of the input, used when submitting an HTML form |
fullWidth | boolean | false | Whether the autocomplete should take full width of its container |
variant | "primary" | "secondary" | "primary" | Visual variant of the component. primary is the default style with shadow. secondary is a lower emphasis variant without shadow. |
class | string | - | Additional CSS classes |
children | JSX.Element | - | Autocomplete content |
Autocomplete.Trigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | - | Trigger content |
Autocomplete.Value Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | (state: AutocompleteValueState) => JSX.Element | - | Value content or render function |
Autocomplete.Indicator Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
children | JSX.Element | - | Custom indicator content |
Autocomplete.ClearButton Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Additional CSS classes |
onClick | (e: MouseEvent) => void | - | Handler called when button is clicked |
Autocomplete.Popover Props
| Prop | Type | Default | Description |
|---|---|---|---|
placement | string | "bottom" | Placement relative to the trigger (Kobalte placements, e.g. "top") |
class | string | - | Additional CSS classes |
children | JSX.Element | - | Content children. Wrapped internally in a dialog element for focus management. |
Autocomplete.Filter Props
| Prop | Type | Default | Description |
|---|---|---|---|
filter | (text: string, input: string) => boolean | - | Custom filter function |
inputValue | string | - | Controlled input value |
onInputChange | (value: string) => void | - | Handler called when input value changes |
children | JSX.Element | - | Filter content (SearchField and ListBox) |
useFilter Hook
The useFilter hook provides filtering functions for autocomplete functionality.
import { useFilter } from "heroui-solid";
const { contains } = useFilter({ sensitivity: "base" });
<Autocomplete.Filter filter={contains}>
<SearchField>...</SearchField>
<ListBox>...</ListBox>
</Autocomplete.Filter>;Options:
| Option | Type | Default | Description |
|---|---|---|---|
sensitivity | "base" | "accent" | "case" | "variant" | "base" | Locale sensitivity for matching |
Returns:
| Function | Type | Description |
|---|---|---|
contains | (string: string, substring: string) => boolean | Returns whether a string contains a given substring |
startsWith | (string: string, substring: string) => boolean | Returns whether a string starts with a given substring |
endsWith | (string: string, substring: string) => boolean | Returns whether a string ends with a given substring |
RenderProps
When using a render function with Autocomplete.Value, these values are provided:
| Prop | Type | Description |
|---|---|---|
defaultChildren | JSX.Element | The default rendered value |
isPlaceholder | boolean | Whether the value is a placeholder |
state | SelectState | The state of the autocomplete |
Accessibility
The Autocomplete component implements the ARIA select pattern with filtering and provides:
- Full keyboard navigation support
- Screen reader announcements for selection changes
- Focus management aligned with Popover:
Autocomplete.Popoverwraps its content in an internal dialog so touch interactions do not show a stray focus ring on the popover overlay - Support for disabled states
- Search functionality with filtering
- HTML form integration
Use autoFocus={false} on SearchField when you want to avoid opening the mobile keyboard as soon as the popover appears. Filtering still works once the user focuses the search input.
Differences from HeroUI React
- Uses
classinstead ofclassName, andonClickinstead of React Aria'sonPress. Autocomplete.Valuerender props receive{ defaultChildren, isPlaceholder, state };state.selectedItemsexposes the selected nodes (each with akey).Autocomplete.Popoverplacements use Kobalte's syntax ("top","bottom-start", …) instead of React Aria's ("top left").- The asynchronous filtering example adapts React Aria's
useAsyncListto Solid'screateResourcewith a debounced, controlledinputValue.
Last updated: 7/19/26, 3:27 AM