Autocomplete
A component that suggests options as the user types.
autocomplete-demo.tsx
import * as React from "react"
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
} from "@/components/ui/autocomplete"
import { Label } from "@/components/ui/label"
export function AutocompleteDemo() {
return (
<div className="w-80">
<Autocomplete items={tags}>
<div className="flex flex-col gap-2">
<Label htmlFor="search-tags">Search tags</Label>
<AutocompleteInput
id="search-tags"
placeholder="e.g. feature, fix, bug"
/>
</div>
<AutocompleteContent>
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(tag: Tag) => (
<AutocompleteItem key={tag.id} value={tag}>
{tag.value}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
</div>
)
}
interface Tag {
id: string
value: string
}
const tags: Tag[] = [
{ id: "t1", value: "feature" },
{ id: "t2", value: "fix" },
{ id: "t3", value: "bug" },
{ id: "t4", value: "docs" },
{ id: "t5", value: "internal" },
{ id: "t6", value: "mobile" },
{ id: "c-accordion", value: "component: accordion" },
{ id: "c-alert-dialog", value: "component: alert dialog" },
{ id: "c-autocomplete", value: "component: autocomplete" },
{ id: "c-avatar", value: "component: avatar" },
{ id: "c-checkbox", value: "component: checkbox" },
{ id: "c-checkbox-group", value: "component: checkbox group" },
{ id: "c-collapsible", value: "component: collapsible" },
{ id: "c-combobox", value: "component: combobox" },
{ id: "c-context-menu", value: "component: context menu" },
{ id: "c-dialog", value: "component: dialog" },
{ id: "c-field", value: "component: field" },
{ id: "c-form", value: "component: form" },
{ id: "c-input", value: "component: input" },
{ id: "c-popover", value: "component: popover" },
{ id: "c-select", value: "component: select" },
{ id: "c-switch", value: "component: switch" },
{ id: "c-tabs", value: "component: tabs" },
{ id: "c-tooltip", value: "component: tooltip" },
]Installation
npx shadcn@latest add @9ui/autocomplete
Usage
Imports
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/autocomplete"Anatomy
<Autocomplete>
<AutocompleteItem>
<AccordionTrigger />
<AccordionContent />
</AutocompleteItem>
</Autocomplete>Examples
Groupped
autocomplete-groupped.tsx
import * as React from "react"
import {
Autocomplete,
AutocompleteCollection,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteSeparator,
AutocompleteValue,
} from "@/components/ui/autocomplete"
import { Label } from "@/components/ui/label"
export function AutocompleteGroupped() {
// Filter and group items by category
const groupedItems = React.useMemo(() => {
const groups: { [key: string]: Item[] } = items.reduce(
(acc, item) => {
acc[item.category] = acc[item.category] || []
acc[item.category].push(item)
return acc
},
{} as { [key: string]: Item[] }
)
const order = [
"Frontend Frameworks",
"Backend Runtime",
"Backend Frameworks",
"Meta Frameworks",
]
return order.map((value) => ({ value, items: groups[value] ?? [] }))
}, [])
return (
<div className="w-80">
<Autocomplete items={groupedItems}>
<div className="flex flex-col gap-2">
<Label htmlFor="search-technologies">Search technologies</Label>
<AutocompleteInput
id="search-technologies"
placeholder="e.g. React, Vue, Angular"
/>
</div>
<AutocompleteContent>
<AutocompleteEmpty>
No technologies found for "
<AutocompleteValue />
"
</AutocompleteEmpty>
<AutocompleteList>
{(group: Group, index: number) => (
<React.Fragment key={group.value}>
{index > 0 && <AutocompleteSeparator />}
<AutocompleteGroup items={group.items}>
<AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
<AutocompleteCollection>
{(item: Item) => (
<AutocompleteItem key={item.id} value={item.value}>
{item.value}
</AutocompleteItem>
)}
</AutocompleteCollection>
</AutocompleteGroup>
</React.Fragment>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
</div>
)
}
interface Group {
value: string
items: Item[]
}
interface Item {
id: string
value: string
category: string
}
const items: Item[] = [
{ id: "react", value: "React", category: "Frontend Frameworks" },
{ id: "vue", value: "Vue.js", category: "Frontend Frameworks" },
{ id: "angular", value: "Angular", category: "Frontend Frameworks" },
{ id: "svelte", value: "Svelte", category: "Frontend Frameworks" },
{ id: "nodejs", value: "Node.js", category: "Backend Runtime" },
{ id: "deno", value: "Deno", category: "Backend Runtime" },
{ id: "bun", value: "Bun", category: "Backend Runtime" },
{ id: "express", value: "Express.js", category: "Backend Frameworks" },
{ id: "fastify", value: "Fastify", category: "Backend Frameworks" },
{ id: "nestjs", value: "NestJS", category: "Backend Frameworks" },
{ id: "nextjs", value: "Next.js", category: "Meta Frameworks" },
{ id: "nuxt", value: "Nuxt.js", category: "Meta Frameworks" },
{ id: "remix", value: "Remix", category: "Meta Frameworks" },
]Async
autocomplete-async.tsx
"use client"
import * as React from "react"
import { Loader2Icon } from "lucide-react"
import {
Autocomplete,
AutocompleteContent,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteStatus,
useFilter,
} from "@/components/ui/autocomplete"
import { Label } from "@/components/ui/label"
export function AutocompleteAsync() {
const [searchValue, setSearchValue] = React.useState("")
const [isLoading, setIsLoading] = React.useState(false)
const [searchResults, setSearchResults] = React.useState<Book[]>([])
const [error, setError] = React.useState<string | null>(null)
const { contains } = useFilter({ sensitivity: "base" })
React.useEffect(() => {
if (!searchValue) {
setSearchResults([])
setIsLoading(false)
return undefined
}
setIsLoading(true)
setError(null)
let ignore = false
async function fetchBooks() {
try {
const results = await searchBooks(searchValue, contains)
if (!ignore) {
setSearchResults(results)
}
} catch {
if (!ignore) {
setError("Failed to find books. Please try again.")
setSearchResults([])
}
} finally {
if (!ignore) {
setIsLoading(false)
}
}
}
const timeoutId = setTimeout(fetchBooks, 300)
return () => {
clearTimeout(timeoutId)
ignore = true
}
}, [searchValue, contains])
let status: React.ReactNode = `${searchResults.length} book${
searchResults.length === 1 ? "" : "s"
} found`
if (isLoading) {
status = (
<React.Fragment>
<Loader2Icon className="size-4 animate-spin" />
Searching books...
</React.Fragment>
)
} else if (error) {
status = error
} else if (searchResults.length === 0 && searchValue) {
status = `No books found for "${searchValue}"`
}
const shouldRenderPopup = searchValue !== ""
return (
<div className="w-80">
<Autocomplete
items={searchResults}
value={searchValue}
onValueChange={setSearchValue}
itemToStringValue={(item: unknown) => (item as Book).title}
filter={null}
>
<div className="flex flex-col gap-2">
<Label htmlFor="search-books">Search books</Label>
<AutocompleteInput
id="search-books"
placeholder="e.g. Hamlet or Shakespeare or 1603"
/>
</div>
{shouldRenderPopup && (
<AutocompleteContent aria-busy={isLoading || undefined}>
<AutocompleteStatus>{status}</AutocompleteStatus>
<AutocompleteList>
{(book: Book) => (
<AutocompleteItem key={book.id} value={book}>
<div className="flex w-full flex-col gap-1">
<div className="font-medium">{book.title}</div>
<div className="text-muted-foreground text-xs">
by {book.author}, {book.publishedYear}
</div>
</div>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
)}
</Autocomplete>
</div>
)
}
interface Book {
id: string
title: string
author: string
publishedYear: number
}
const books: Book[] = [
{
id: "1",
title: "Pride and Prejudice",
author: "Jane Austen",
publishedYear: 1813,
},
{
id: "2",
title: "To Kill a Mockingbird",
author: "Harper Lee",
publishedYear: 1960,
},
{ id: "3", title: "1984", author: "George Orwell", publishedYear: 1949 },
{
id: "4",
title: "The Great Gatsby",
author: "F. Scott Fitzgerald",
publishedYear: 1925,
},
{
id: "5",
title: "Jane Eyre",
author: "Charlotte Brontë",
publishedYear: 1847,
},
{
id: "6",
title: "Wuthering Heights",
author: "Emily Brontë",
publishedYear: 1847,
},
{
id: "7",
title: "The Catcher in the Rye",
author: "J.D. Salinger",
publishedYear: 1951,
},
{
id: "8",
title: "Lord of the Flies",
author: "William Golding",
publishedYear: 1954,
},
{
id: "9",
title: "Of Mice and Men",
author: "John Steinbeck",
publishedYear: 1937,
},
{
id: "10",
title: "Romeo and Juliet",
author: "William Shakespeare",
publishedYear: 1597,
},
{
id: "11",
title: "The Adventures of Huckleberry Finn",
author: "Mark Twain",
publishedYear: 1884,
},
{
id: "12",
title: "The Lord of the Rings",
author: "J.R.R. Tolkien",
publishedYear: 1954,
},
{
id: "13",
title: "Animal Farm",
author: "George Orwell",
publishedYear: 1945,
},
{
id: "14",
title: "Brave New World",
author: "Aldous Huxley",
publishedYear: 1932,
},
{
id: "15",
title: "The Picture of Dorian Gray",
author: "Oscar Wilde",
publishedYear: 1890,
},
{
id: "16",
title: "Crime and Punishment",
author: "Fyodor Dostoevsky",
publishedYear: 1866,
},
{
id: "17",
title: "The Brothers Karamazov",
author: "Fyodor Dostoevsky",
publishedYear: 1880,
},
{
id: "18",
title: "War and Peace",
author: "Leo Tolstoy",
publishedYear: 1869,
},
{
id: "19",
title: "Anna Karenina",
author: "Leo Tolstoy",
publishedYear: 1877,
},
{ id: "20", title: "The Odyssey", author: "Homer", publishedYear: -800 },
{ id: "21", title: "The Iliad", author: "Homer", publishedYear: -750 },
{
id: "22",
title: "Hamlet",
author: "William Shakespeare",
publishedYear: 1603,
},
{
id: "23",
title: "Macbeth",
author: "William Shakespeare",
publishedYear: 1623,
},
{
id: "24",
title: "One Hundred Years of Solitude",
author: "Gabriel García Márquez",
publishedYear: 1967,
},
{
id: "25",
title: "The Divine Comedy",
author: "Dante Alighieri",
publishedYear: 1320,
},
{
id: "26",
title: "Don Quixote",
author: "Miguel de Cervantes",
publishedYear: 1605,
},
{
id: "27",
title: "Moby Dick",
author: "Herman Melville",
publishedYear: 1851,
},
{
id: "28",
title: "The Scarlet Letter",
author: "Nathaniel Hawthorne",
publishedYear: 1850,
},
{
id: "29",
title: "The Canterbury Tales",
author: "Geoffrey Chaucer",
publishedYear: 1400,
},
{
id: "30",
title: "Great Expectations",
author: "Charles Dickens",
publishedYear: 1861,
},
{
id: "31",
title: "A Tale of Two Cities",
author: "Charles Dickens",
publishedYear: 1859,
},
{
id: "32",
title: "Oliver Twist",
author: "Charles Dickens",
publishedYear: 1838,
},
{
id: "33",
title: "David Copperfield",
author: "Charles Dickens",
publishedYear: 1850,
},
{
id: "34",
title: "Little Women",
author: "Louisa May Alcott",
publishedYear: 1868,
},
{
id: "35",
title: "The Count of Monte Cristo",
author: "Alexandre Dumas",
publishedYear: 1844,
},
{
id: "36",
title: "Les Misérables",
author: "Victor Hugo",
publishedYear: 1862,
},
{
id: "37",
title: "The Hunchback of Notre-Dame",
author: "Victor Hugo",
publishedYear: 1831,
},
{
id: "38",
title: "Madame Bovary",
author: "Gustave Flaubert",
publishedYear: 1857,
},
{
id: "39",
title: "The Stranger",
author: "Albert Camus",
publishedYear: 1942,
},
{
id: "40",
title: "The Metamorphosis",
author: "Franz Kafka",
publishedYear: 1915,
},
]
async function searchBooks(
query: string,
filter: (item: string, query: string) => boolean
): Promise<Book[]> {
// Simulate network delay
await new Promise((resolve) => {
setTimeout(resolve, Math.random() * 400 + 200)
})
// Simulate occasional network errors (1% chance)
if (Math.random() < 0.01 || query === "will_error") {
throw new Error("Network error")
}
return books.filter(
(book) =>
filter(book.title, query) ||
filter(book.author, query) ||
filter(book.publishedYear.toString(), query)
)
}Inline Autocomplete
autocomplete-inline.tsx
import * as React from "react"
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
} from "@/components/ui/autocomplete"
import { Label } from "@/components/ui/label"
export function AutocompleteInline() {
return (
<div className="w-80">
<Autocomplete items={tags} mode="both">
<div className="flex flex-col gap-2">
<Label htmlFor="search-tags-inline">Search tags</Label>
<AutocompleteInput
id="search-tags-inline"
placeholder="e.g. feature, fix, bug"
/>
</div>
<AutocompleteContent>
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(tag: Tag) => (
<AutocompleteItem key={tag.id} value={tag}>
{tag.value}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
</div>
)
}
interface Tag {
id: string
value: string
}
const tags: Tag[] = [
{ id: "t1", value: "feature" },
{ id: "t2", value: "fix" },
{ id: "t3", value: "bug" },
{ id: "t4", value: "docs" },
{ id: "t5", value: "internal" },
{ id: "t6", value: "mobile" },
{ id: "c-accordion", value: "component: accordion" },
{ id: "c-alert-dialog", value: "component: alert dialog" },
{ id: "c-autocomplete", value: "component: autocomplete" },
{ id: "c-avatar", value: "component: avatar" },
{ id: "c-checkbox", value: "component: checkbox" },
{ id: "c-checkbox-group", value: "component: checkbox group" },
{ id: "c-collapsible", value: "component: collapsible" },
{ id: "c-combobox", value: "component: combobox" },
{ id: "c-context-menu", value: "component: context menu" },
{ id: "c-dialog", value: "component: dialog" },
{ id: "c-field", value: "component: field" },
{ id: "c-form", value: "component: form" },
{ id: "c-input", value: "component: input" },
{ id: "c-popover", value: "component: popover" },
{ id: "c-select", value: "component: select" },
{ id: "c-switch", value: "component: switch" },
{ id: "c-tabs", value: "component: tabs" },
{ id: "c-tooltip", value: "component: tooltip" },
]Fuzzy Matcher
autocomplete-fuzzy-matcher.tsx
"use client"
import * as React from "react"
import { matchSorter } from "match-sorter"
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteValue,
} from "@/components/ui/autocomplete"
import { Label } from "@/components/ui/label"
export function AutocompleteFuzzyMatcher() {
const fuzzyFilter = React.useCallback(
(item: unknown, query: string): boolean => {
const doc = item as DevDoc
if (!query) {
return true
}
const results = matchSorter([doc], query, {
keys: ["title", "description"],
threshold: matchSorter.rankings.MATCHES,
})
return results.length > 0
},
[]
)
return (
<div className="w-80">
<Autocomplete
items={devDocs}
filter={fuzzyFilter}
itemToStringValue={(item: unknown) => (item as DevDoc).title}
>
<div className="flex flex-col gap-2">
<Label htmlFor="search-docs">Search developer docs</Label>
<AutocompleteInput
id="search-docs"
placeholder="e.g. react hooks, api"
/>
</div>
<AutocompleteContent className="w-80">
<AutocompleteEmpty>
No docs found for "
<AutocompleteValue />
"
</AutocompleteEmpty>
<AutocompleteList>
{(doc: DevDoc) => (
<AutocompleteItem key={doc.id} value={doc}>
<AutocompleteValue>
{(value) => (
<div className="flex w-full flex-col gap-1">
<div className="text-sm font-medium">
{highlightText(doc.title, value)}
</div>
<div className="text-muted-foreground text-xs leading-relaxed">
{highlightText(doc.description, value)}
</div>
</div>
)}
</AutocompleteValue>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
</div>
)
}
function highlightText(text: string, query: string): React.ReactNode {
const trimmed = query.trim()
if (!trimmed) {
return text
}
const limited = trimmed.slice(0, 100)
const escaped = limited.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
const regex = new RegExp(`(${escaped})`, "gi")
return text.split(regex).map((part, idx) =>
regex.test(part) ? (
<mark key={idx} className="bg-transparent font-medium text-yellow-300">
{part}
</mark>
) : (
part
)
)
}
interface DevDoc {
id: string
title: string
description: string
}
const devDocs: DevDoc[] = [
{
id: "1",
title: "React Hooks Guide",
description:
"Learn how to use React Hooks like useState, useEffect, and custom hooks to manage state and side effects in functional components.",
},
{
id: "2",
title: "JavaScript Array Methods",
description:
"Master array methods like map, filter, reduce, and forEach for functional programming in JavaScript.",
},
{
id: "3",
title: "CSS Flexbox Layout",
description:
"Complete guide to CSS Flexbox for creating responsive and flexible layouts with ease.",
},
{
id: "4",
title: "TypeScript Interfaces",
description:
"Understanding TypeScript interfaces and type definitions for better code safety and documentation.",
},
{
id: "5",
title: "API Design Best Practices",
description:
"Learn how to design RESTful APIs that are intuitive, scalable, and maintainable.",
},
{
id: "6",
title: "React Performance Optimization",
description:
"Tips and techniques for optimizing React application performance using memoization and lazy loading.",
},
{
id: "7",
title: "Git Workflow Strategies",
description:
"Understanding different Git workflows like GitFlow, GitHub Flow, and trunk-based development.",
},
{
id: "8",
title: "Node.js Express Server",
description:
"Building RESTful APIs with Node.js and Express framework for scalable backend applications.",
},
{
id: "9",
title: "Database Indexing",
description:
"How to use database indexes effectively to improve query performance and reduce response times.",
},
{
id: "10",
title: "Docker Containerization",
description:
"Learn how to containerize applications with Docker for consistent deployment across environments.",
},
{
id: "11",
title: "Authentication & Authorization",
description:
"Implementing secure authentication and authorization using JWT tokens and OAuth protocols.",
},
{
id: "12",
title: "Testing Strategies",
description:
"Comprehensive guide to unit testing, integration testing, and end-to-end testing practices.",
},
{
id: "13",
title: "Webpack Configuration",
description:
"Optimizing Webpack configuration for production builds and development workflows.",
},
{
id: "14",
title: "Microservices Architecture",
description:
"Designing and implementing microservices architecture for scalable distributed systems.",
},
{
id: "15",
title: "GraphQL API Development",
description:
"Building efficient GraphQL APIs with type safety and flexible data fetching capabilities.",
},
]Auto Highlight
autocomplete-autohighlight.tsx
import * as React from "react"
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
} from "@/components/ui/autocomplete"
import { Label } from "@/components/ui/label"
export function AutocompleteAutoHighlight() {
return (
<div className="w-80">
<Autocomplete items={tags} autoHighlight>
<div className="flex flex-col gap-2">
<Label htmlFor="search-tags-autohighlight">Search tags</Label>
<AutocompleteInput
id="search-tags-autohighlight"
placeholder="e.g. feature, fix, bug"
/>
</div>
<AutocompleteContent>
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(tag: Tag) => (
<AutocompleteItem key={tag.id} value={tag}>
{tag.value}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
</div>
)
}
interface Tag {
id: string
value: string
}
const tags: Tag[] = [
{ id: "t1", value: "feature" },
{ id: "t2", value: "fix" },
{ id: "t3", value: "bug" },
{ id: "t4", value: "docs" },
{ id: "t5", value: "internal" },
{ id: "t6", value: "mobile" },
{ id: "c-accordion", value: "component: accordion" },
{ id: "c-alert-dialog", value: "component: alert dialog" },
{ id: "c-autocomplete", value: "component: autocomplete" },
{ id: "c-avatar", value: "component: avatar" },
{ id: "c-checkbox", value: "component: checkbox" },
{ id: "c-checkbox-group", value: "component: checkbox group" },
{ id: "c-collapsible", value: "component: collapsible" },
{ id: "c-combobox", value: "component: combobox" },
{ id: "c-context-menu", value: "component: context menu" },
{ id: "c-dialog", value: "component: dialog" },
{ id: "c-field", value: "component: field" },
{ id: "c-form", value: "component: form" },
{ id: "c-input", value: "component: input" },
{ id: "c-popover", value: "component: popover" },
{ id: "c-select", value: "component: select" },
{ id: "c-switch", value: "component: switch" },
{ id: "c-tabs", value: "component: tabs" },
{ id: "c-tooltip", value: "component: tooltip" },
]Limit Results
autocomplete-limit-results.tsx
import * as React from "react"
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteStatus,
useFilter,
} from "@/components/ui/autocomplete"
import { Label } from "@/components/ui/label"
const limit = 8
export function AutocompleteLimitResults() {
const [value, setValue] = React.useState("")
const { contains } = useFilter({ sensitivity: "base" })
const totalMatches = React.useMemo(() => {
const trimmed = value.trim()
if (!trimmed) {
return tags.length
}
return tags.filter((t) => contains(t.value, trimmed)).length
}, [value, contains])
const moreCount = Math.max(0, totalMatches - limit)
return (
<div className="w-80">
<Autocomplete
items={tags}
value={value}
onValueChange={setValue}
limit={limit}
>
<div className="flex flex-col gap-2">
<Label htmlFor="search-tags-limit">
Search tags (limited to {limit})
</Label>
<AutocompleteInput
id="search-tags-limit"
placeholder="e.g. component"
/>
</div>
<AutocompleteContent className="w-80">
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(tag: Tag) => (
<AutocompleteItem key={tag.id} value={tag}>
{tag.value}
</AutocompleteItem>
)}
</AutocompleteList>
{moreCount > 0 && (
<AutocompleteStatus>
{moreCount} results hidden (type a more specific query to narrow
results)
</AutocompleteStatus>
)}
</AutocompleteContent>
</Autocomplete>
</div>
)
}
interface Tag {
id: string
value: string
}
const tags: Tag[] = [
{ id: "t1", value: "feature" },
{ id: "t2", value: "fix" },
{ id: "t3", value: "bug" },
{ id: "t4", value: "docs" },
{ id: "t5", value: "internal" },
{ id: "t6", value: "mobile" },
{ id: "c-accordion", value: "component: accordion" },
{ id: "c-alert-dialog", value: "component: alert dialog" },
{ id: "c-autocomplete", value: "component: autocomplete" },
{ id: "c-avatar", value: "component: avatar" },
{ id: "c-checkbox", value: "component: checkbox" },
{ id: "c-checkbox-group", value: "component: checkbox group" },
{ id: "c-collapsible", value: "component: collapsible" },
{ id: "c-combobox", value: "component: combobox" },
{ id: "c-context-menu", value: "component: context menu" },
{ id: "c-dialog", value: "component: dialog" },
{ id: "c-field", value: "component: field" },
{ id: "c-form", value: "component: form" },
{ id: "c-input", value: "component: input" },
{ id: "c-popover", value: "component: popover" },
{ id: "c-select", value: "component: select" },
{ id: "c-switch", value: "component: switch" },
{ id: "c-tabs", value: "component: tabs" },
{ id: "c-tooltip", value: "component: tooltip" },
]With Rows
autocomplete-row.tsx
"use client"
import * as React from "react"
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteRow,
AutocompleteTrigger,
} from "@/components/ui/autocomplete"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Label } from "@/components/ui/label"
export function AutocompleteRowDemo() {
const [pickerOpen, setPickerOpen] = React.useState(false)
const [avatar, setAvatar] = React.useState<string | null>(null)
const [searchValue, setSearchValue] = React.useState("")
function handleSelectAvatar(value: string | null) {
if (!value) {
return
}
setPickerOpen(false)
setAvatar(value)
setSearchValue("")
}
return (
<Autocomplete
items={avatarGroups}
cols={COLUMNS}
open={pickerOpen}
onOpenChange={setPickerOpen}
onOpenChangeComplete={() => setSearchValue("")}
value={searchValue}
onValueChange={(value, details) => {
if (details.reason !== "item-press") {
setSearchValue(value)
}
}}
>
<AutocompleteTrigger
className="flex flex-col items-center gap-2 outline-none"
aria-label="Choose avatar"
>
<Avatar id="avatar-selector">
<AvatarFallback>{avatar || "+"}</AvatarFallback>
</Avatar>
<Label
className="text-muted-foreground text-xs"
htmlFor="avatar-selector"
>
Choose an avatar
</Label>
</AutocompleteTrigger>
<AutocompleteContent className="w-80">
<AutocompleteInput placeholder="e.g. cat, doctor, star" />
<AutocompleteEmpty>No avatars found</AutocompleteEmpty>
<AutocompleteList>
{(group: AvatarGroup) => (
<AutocompleteGroup
className="mt-2"
key={group.value}
items={group.items}
>
<AutocompleteGroupLabel>{group.label}</AutocompleteGroupLabel>
<div role="presentation">
{chunkArray(group.items, COLUMNS).map((row, rowIdx) => (
<AutocompleteRow key={rowIdx} className="grid-cols-6">
{row.map((avatar) => (
<AutocompleteItem
key={avatar.id}
value={avatar}
className="flex aspect-square size-full items-center justify-center rounded-md"
onClick={() => handleSelectAvatar(avatar.emoji)}
>
<span className="text-xl">{avatar.emoji}</span>
</AutocompleteItem>
))}
</AutocompleteRow>
))}
</div>
</AutocompleteGroup>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
)
}
interface Avatar {
id: string
emoji: string
name: string
category: string
}
interface AvatarGroup {
value: string
label: string
items: Avatar[]
}
const avatarCategories = [
{
label: "People",
avatars: [
{ id: "1", emoji: "👤", name: "default user" },
{ id: "2", emoji: "👨", name: "man" },
{ id: "3", emoji: "👩", name: "woman" },
{ id: "4", emoji: "🧑", name: "person" },
{ id: "5", emoji: "👦", name: "boy" },
{ id: "6", emoji: "👧", name: "girl" },
{ id: "7", emoji: "👴", name: "old man" },
{ id: "8", emoji: "👵", name: "old woman" },
{ id: "9", emoji: "👶", name: "baby" },
{ id: "10", emoji: "🧒", name: "child" },
{ id: "11", emoji: "🧓", name: "older person" },
{ id: "12", emoji: "👨💼", name: "businessman" },
{ id: "13", emoji: "👩💼", name: "businesswoman" },
{ id: "14", emoji: "👨💻", name: "developer" },
{ id: "15", emoji: "👩💻", name: "developer woman" },
{ id: "16", emoji: "👨🎨", name: "artist" },
],
},
{
label: "Professions",
avatars: [
{ id: "17", emoji: "👩🎨", name: "artist woman" },
{ id: "18", emoji: "👨⚕️", name: "doctor" },
{ id: "19", emoji: "👩⚕️", name: "doctor woman" },
{ id: "20", emoji: "👨🏫", name: "teacher" },
{ id: "21", emoji: "👩🏫", name: "teacher woman" },
{ id: "22", emoji: "👨🚀", name: "astronaut" },
{ id: "23", emoji: "👩🚀", name: "astronaut woman" },
{ id: "24", emoji: "👨🔬", name: "scientist" },
{ id: "25", emoji: "👩🔬", name: "scientist woman" },
{ id: "26", emoji: "👨🍳", name: "chef" },
{ id: "27", emoji: "👩🍳", name: "chef woman" },
{ id: "28", emoji: "👨🎤", name: "singer" },
{ id: "29", emoji: "👩🎤", name: "singer woman" },
{ id: "30", emoji: "👨✈️", name: "pilot" },
{ id: "31", emoji: "👩✈️", name: "pilot woman" },
{ id: "32", emoji: "👮", name: "police officer" },
],
},
{
label: "Animals",
avatars: [
{ id: "33", emoji: "🐶", name: "dog" },
{ id: "34", emoji: "🐱", name: "cat" },
{ id: "35", emoji: "🐭", name: "mouse" },
{ id: "36", emoji: "🐹", name: "hamster" },
{ id: "37", emoji: "🐰", name: "rabbit" },
{ id: "38", emoji: "🦊", name: "fox" },
{ id: "39", emoji: "🐻", name: "bear" },
{ id: "40", emoji: "🐼", name: "panda" },
{ id: "41", emoji: "🐨", name: "koala" },
{ id: "42", emoji: "🐯", name: "tiger" },
{ id: "43", emoji: "🦁", name: "lion" },
{ id: "44", emoji: "🐮", name: "cow" },
{ id: "45", emoji: "🐷", name: "pig" },
{ id: "46", emoji: "🐸", name: "frog" },
{ id: "47", emoji: "🐵", name: "monkey" },
{ id: "48", emoji: "🐧", name: "penguin" },
],
},
{
label: "Objects & Symbols",
avatars: [
{ id: "49", emoji: "⭐", name: "star" },
{ id: "50", emoji: "🌟", name: "glowing star" },
{ id: "51", emoji: "💎", name: "diamond" },
{ id: "52", emoji: "🔥", name: "fire" },
{ id: "53", emoji: "⚡", name: "lightning" },
{ id: "54", emoji: "🌙", name: "moon" },
{ id: "55", emoji: "☀️", name: "sun" },
{ id: "56", emoji: "🌈", name: "rainbow" },
{ id: "57", emoji: "🎯", name: "target" },
{ id: "58", emoji: "🚀", name: "rocket" },
{ id: "59", emoji: "🎮", name: "gaming" },
{ id: "60", emoji: "💻", name: "laptop" },
{ id: "61", emoji: "📱", name: "phone" },
{ id: "62", emoji: "🎨", name: "art" },
{ id: "63", emoji: "🏆", name: "trophy" },
{ id: "64", emoji: "🎪", name: "circus" },
],
},
]
const avatarGroups: AvatarGroup[] = avatarCategories.map((category) => ({
value: category.label,
label: category.label,
items: category.avatars.map((avatar) => ({
...avatar,
value: avatar.name.toLowerCase(),
category: category.label,
})),
}))
const COLUMNS = 6
function chunkArray<T>(array: T[], size: number): T[][] {
const result: T[][] = []
for (let i = 0; i < array.length; i += size) {
result.push(array.slice(i, i + size))
}
return result
}Virtualized
autocomplete-virtualized.tsx
import * as React from "react"
import { useVirtualizer } from "@tanstack/react-virtual"
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
useFilter,
} from "@/components/ui/autocomplete"
import { Label } from "@/components/ui/label"
export function AutocompleteVirtualized() {
const [open, setOpen] = React.useState(false)
const [searchValue, setSearchValue] = React.useState("")
const scrollElementRef = React.useRef<HTMLDivElement>(null)
const { contains } = useFilter({ sensitivity: "base" })
const filteredItems = React.useMemo(() => {
return virtualItems.filter((item) => contains(item, searchValue))
}, [contains, searchValue])
const virtualizer = useVirtualizer({
enabled: open,
count: filteredItems.length,
getScrollElement: () => scrollElementRef.current,
estimateSize: () => 32,
overscan: 20,
paddingStart: 4,
paddingEnd: 4,
})
const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement) => {
scrollElementRef.current = element
if (element) {
virtualizer.measure()
}
},
[virtualizer]
)
const totalSize = virtualizer.getTotalSize()
const totalSizePx = `${totalSize}px`
return (
<Autocomplete
virtualized
items={virtualItems}
open={open}
onOpenChange={setOpen}
value={searchValue}
onValueChange={setSearchValue}
openOnInputClick
onItemHighlighted={(item, { type, index }) => {
if (!item) {
return
}
const isStart = index === 0
const isEnd = index === filteredItems.length - 1
const shouldScroll =
type === "none" || (type === "keyboard" && (isStart || isEnd))
if (shouldScroll) {
queueMicrotask(() => {
virtualizer.scrollToIndex(index, { align: isEnd ? "start" : "end" })
})
}
}}
>
<div className="flex flex-col gap-2">
<Label htmlFor="search-items-virtualized">
Search 10,000 items (virtualized)
</Label>
<AutocompleteInput className="w-80" id="search-items-virtualized" />
</div>
<AutocompleteContent className="w-80 py-0 pr-0">
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{filteredItems.length > 0 && (
<div
role="presentation"
ref={handleScrollElementRef}
className="h-[min(var(--total-size),18rem)] max-h-[calc(var(--available-height)-2rem)] overflow-y-scroll overscroll-contain"
style={{ "--total-size": totalSizePx } as React.CSSProperties}
>
<div
role="presentation"
className="relative w-full"
style={{ height: totalSizePx }}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = filteredItems[virtualItem.index]
if (!item) {
return null
}
return (
<AutocompleteItem
key={virtualItem.key}
index={virtualItem.index}
value={item}
aria-setsize={filteredItems.length}
aria-posinset={virtualItem.index + 1}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{item}
</AutocompleteItem>
)
})}
</div>
</div>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
)
}
const virtualItems = Array.from({ length: 10000 }, (_, i) => {
const indexLabel = String(i + 1).padStart(5, "0")
return `Item ${indexLabel}`
})