Components
HeroUI v3 Autocomplete is a searchable select that filters a ListBox of options as you type, supporting single and multiple selection with removable tags, grouped sections, disabled options, descriptions, form validation, controlled state, custom trigger values with avatars, and full-width layouts inside a Surface. Built on the real @heroui/react package with @heroui/styles, it composes with SearchField, ListBox, TagGroup, Label, Description, FieldError and useFilter. A thin re-export of the upstream component so consumers install and render the genuine HeroUI primitive.
npx shadcn@latest add https://21st.dev/r/hero_ui/heroui-autocompleteLoading preview...
"use client"
import type { Key } from "@heroui/react"
import {
Avatar,
AvatarFallback,
AvatarImage,
Description,
EmptyState,
Label,
ListBox,
SearchField,
useFilter,
} from "@heroui/react"
import { useState } from "react"
import { Autocomplete } from "@/components/ui/heroui-autocomplete"
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] = useState<Key | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
return (
<Autocomplete
className="w-[256px]"
placeholder="Select a user"
selectionMode="single"
value={selectedKey}
onChange={setSelectedKey}
>
<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 className="flex items-center gap-2">
<Avatar className="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>}>
{users.map((user) => (
<ListBox.Item key={user.id} id={user.id} textValue={user.name}>
<Avatar size="sm">
<AvatarImage src={user.avatarUrl} />
<AvatarFallback>{user.fallback}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<Label>{user.name}</Label>
<Description>{user.email}</Description>
</div>
<ListBox.ItemIndicator />
</ListBox.Item>
))}
</ListBox>
</Autocomplete.Filter>
</Autocomplete.Popover>
</Autocomplete>
)
}
export default UserSelection
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...
Loading preview...