GitHub

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 &quot;
						<AutocompleteValue />
						&quot;
					</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 &quot;
						<AutocompleteValue />
						&quot;
					</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}`
})