Overview
Get Started
"use client";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
{ label: "Blueberry", value: "blueberry" },
{ label: "Raspberry", value: "raspberry" },
{ label: "Blackberry", value: "blackberry" },
{ label: "Cherry", value: "cherry" },
{ label: "Papaya", value: "papaya" },
{ label: "Cantaloupe", value: "cantaloupe" },
{ label: "Honeydew", value: "honeydew" },
{ label: "Lemon", value: "lemon" },
{ label: "Lime", value: "lime" },
{ label: "Coconut", value: "coconut" },
{ label: "Pomegranate", value: "pomegranate" },
{ label: "Fig", value: "fig" },
{ label: "Date", value: "date" },
];
export function AutocompleteDemo() {
return (
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
inputGroupProps={{ className: "max-w-64" }}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}Installation
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
import { IconSelector, IconX } from "@tabler/icons-react";
import { cn } from "@/lib/cn";
import { ScrollArea } from "@/components/ui/scroll-area";
export const Autocomplete = AutocompletePrimitive.Root;
export function AutocompleteTrigger(
props: React.ComponentProps<typeof AutocompletePrimitive.Trigger>
) {
return <AutocompletePrimitive.Trigger data-slot="autocomplete-trigger" {...props} />;
}
export function AutocompleteClear({
className,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Clear>) {
return (
<AutocompletePrimitive.Clear
data-slot="autocomplete-clear"
className={cn(
"absolute end-0.5 top-1/2 inline-flex size-7 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full outline-none transition-[color,opacity] not-data-disabled:hover:bg-accent hover:opacity-100 data-disabled:cursor-default [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}
>
<IconX />
</AutocompletePrimitive.Clear>
);
}
export const AutocompleteValue = AutocompletePrimitive.Value;
export function AutocompleteInput({
className,
showTrigger = false,
showClear = false,
startAddon,
size = "default",
inputGroupProps,
triggerProps: { className: triggerClassName, ...triggerProps } = {},
clearProps: { className: clearClassName, ...clearProps } = {},
...props
}: Omit<React.ComponentProps<typeof AutocompletePrimitive.Input>, "size"> & {
showTrigger?: boolean;
showClear?: boolean;
startAddon?: React.ReactNode;
size?: "xs" | "sm" | "default" | "lg" | "xl" | number;
inputGroupProps?: React.ComponentProps<typeof AutocompleteInputGroup>;
triggerProps?: React.ComponentProps<typeof AutocompleteTrigger>;
clearProps?: React.ComponentProps<typeof AutocompleteClear>;
}) {
return (
<AutocompleteInputGroup {...inputGroupProps}>
{startAddon && (
<div
aria-hidden="true"
data-slot="autocomplete-start-addon"
className={cn(
"pointer-events-none absolute inset-y-px start-px z-10 flex items-center justify-center ps-2 [&_svg:not([class*='size-'])]:size-4",
size === "xs" && "ps-1.5 [&_svg:not([class*='size-'])]:size-3",
size === "sm" && "ps-1.75 [&_svg:not([class*='size-'])]:size-3.5",
size === "xl" && "ps-2.5 [&_svg:not([class*='size-'])]:size-4.5"
)}
>
{startAddon}
</div>
)}
<AutocompletePrimitive.Input
data-slot="autocomplete-input"
data-size={typeof size === "string" ? size : undefined}
size={typeof size === "number" ? size : undefined}
className={cn(
"h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"aria-invalid:border-danger aria-invalid:ring-danger/30",
size === "xs" && "h-6 rounded-sm px-2 text-xs focus-visible:ring-2",
size === "sm" && "h-7 rounded-md px-2 text-[0.8125rem] focus-visible:ring-2",
size === "lg" && "h-9",
size === "xl" && "h-10 px-3 text-base",
startAddon && "ps-7.5 data-[size=sm]:ps-6 data-[size=xl]:ps-8.5 data-[size=xs]:ps-5.5",
size === "xs" &&
"has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:pe-6",
size === "sm" &&
"has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:pe-7",
size === "default" &&
"has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:pe-8",
size === "lg" &&
"has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:pe-8.5",
size === "xl" &&
"has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:pe-9.5",
className
)}
{...props}
/>
{showTrigger && (
<AutocompleteTrigger
className={cn(
"absolute end-0.5 top-1/2 inline-flex size-7 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md not-data-disabled:opacity-80 outline-none transition-[color,opacity] hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden data-disabled:cursor-default [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
size === "xs" && "size-5 rounded [&_svg:not([class*='size-'])]:size-3.5",
size === "sm" && "size-6 rounded-sm [&_svg:not([class*='size-'])]:size-3.5",
size === "lg" && "end-1 size-7",
size === "xl" && "end-1 size-8 [&_svg:not([class*='size-'])]:size-4.5",
triggerClassName
)}
{...triggerProps}
>
<IconSelector />
</AutocompleteTrigger>
)}
{showClear && (
<AutocompleteClear
className={cn(
"absolute end-0.5 top-1/2 inline-flex size-7 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full outline-none transition-[color,opacity] not-data-disabled:hover:bg-accent hover:opacity-100 data-disabled:cursor-default [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
size === "xs" && "size-5 [&_svg:not([class*='size-'])]:size-3.5",
size === "sm" && "size-6 [&_svg:not([class*='size-'])]:size-3.5",
size === "lg" && "end-1 size-7",
size === "xl" && "end-1 size-8 [&_svg:not([class*='size-'])]:size-4.5",
clearClassName
)}
{...clearProps}
>
<IconX />
</AutocompleteClear>
)}
</AutocompleteInputGroup>
);
}
export function AutocompleteInputGroup({
className,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.InputGroup>) {
return (
<AutocompletePrimitive.InputGroup
data-slot="autocomplete-input-group"
className={cn("relative w-full data-disabled:opacity-50", className)}
{...props}
/>
);
}
export function AutocompletePortal(
props: React.ComponentProps<typeof AutocompletePrimitive.Portal>
) {
return <AutocompletePrimitive.Portal data-slot="autocomplete-portal" {...props} />;
}
export function AutocompletePositioner({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Positioner>) {
return (
<AutocompletePrimitive.Positioner
data-slot="autocomplete-positioner"
sideOffset={sideOffset}
className={cn("z-50 flex select-none", className)}
{...props}
/>
);
}
export function AutocompletePopup({
className,
portalProps,
positionerProps,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Popup> & {
portalProps?: React.ComponentProps<typeof AutocompletePortal>;
positionerProps?: React.ComponentProps<typeof AutocompletePositioner>;
}) {
return (
<AutocompletePortal {...portalProps}>
<AutocompletePositioner {...positionerProps}>
<AutocompletePrimitive.Popup
data-slot="autocomplete-popup"
className={cn(
"flex not-has-data-[slot=scroll-area]:max-h-[min(var(--available-height),23rem)] w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) flex-col overflow-hidden rounded-lg border bg-popover shadow-lg/5 transition-[transform,scale,opacity] data-ending-style:scale-95 data-starting-style:scale-95 data-ending-style:opacity-0 data-starting-style:opacity-0",
className
)}
{...props}
/>
</AutocompletePositioner>
</AutocompletePortal>
);
}
export function AutocompleteList({
className,
scrollArea = true,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.List> & { scrollArea?: boolean }) {
return scrollArea ? (
<ScrollArea scrollbarGutter scrollFade className="max-h-[min(var(--available-height),23rem)]">
<AutocompletePrimitive.List
data-slot="autocomplete-list"
className={cn("not-empty:scroll-py-1 not-empty:p-1", className)}
{...props}
/>
</ScrollArea>
) : (
<AutocompletePrimitive.List
data-slot="autocomplete-list"
className={cn(
"not-empty:scroll-py-1 overflow-y-auto not-empty:p-1 [scrollbar-width:thin]",
className
)}
{...props}
/>
);
}
export function AutocompleteItem({
className,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Item>) {
return (
<AutocompletePrimitive.Item
data-slot="autocomplete-item"
className={cn(
"flex min-h-7 cursor-default select-none items-center rounded-sm px-2 py-1 text-sm outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-50",
className
)}
{...props}
/>
);
}
export function AutocompleteSeparator({
className,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Separator>) {
return (
<AutocompletePrimitive.Separator
data-slot="autocomplete-separator"
className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
{...props}
/>
);
}
export function AutocompleteGroup({
className,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Group>) {
return (
<AutocompletePrimitive.Group
data-slot="autocomplete-group"
className={cn("[[role=group]+&]:mt-1.5", className)}
{...props}
/>
);
}
export function AutocompleteGroupLabel({
className,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.GroupLabel>) {
return (
<AutocompletePrimitive.GroupLabel
data-slot="autocomplete-group-label"
className={cn("px-2 py-1.5 font-medium text-muted-foreground text-xs", className)}
{...props}
/>
);
}
export function AutocompleteEmpty({
className,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Empty>) {
return (
<AutocompletePrimitive.Empty
data-slot="autocomplete-empty"
className={cn("not-empty:p-2 text-center text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export function AutocompleteRow(props: React.ComponentProps<typeof AutocompletePrimitive.Row>) {
return <AutocompletePrimitive.Row data-slot="autocomplete-row" {...props} />;
}
export function AutocompleteStatus({
className,
...props
}: React.ComponentProps<typeof AutocompletePrimitive.Status>) {
return (
<AutocompletePrimitive.Status
data-slot="autocomplete-status"
className={cn(
"px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
className
)}
{...props}
/>
);
}
export const AutocompleteCollection = AutocompletePrimitive.Collection;
export const useAutocompleteFilter = AutocompletePrimitive.useFilter;
export function groupAutocompleteItems<T extends { group: string }>(
tags: T[]
): { value: string; items: T[] }[] {
const groups: Record<string, T[]> = {};
tags.forEach((tag) => {
(groups[tag.group] ??= []).push(tag);
});
return Object.entries(groups).map(([value, items]) => ({
value,
items,
}));
}
export { AutocompletePrimitive };Usage
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";const items = [
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
{ value: "orange", label: "Orange" },
{ value: "grape", label: "Grape" },
]
<Autocomplete items={items}>
<AutocompleteInput placeholder="Search..." />
<AutocompletePopup>
<AutocompleteEmpty>No results found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => <AutocompleteItem key={item.value} value={item}>{item.label}</AutocompleteItem>}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>Examples
Sizes
"use client";
import { IconSearch } from "@tabler/icons-react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
const sizes = ["xs", "sm", "default", "lg", "xl"] as const;
export function AutocompleteDemo() {
return (
<div className="flex flex-col gap-4">
{sizes.map((size) => (
<Autocomplete key={size} items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
size={size}
startAddon={<IconSearch />}
showClear
showTrigger
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
))}
</div>
);
}Disabled
"use client";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
export function AutocompleteDemo() {
return (
<Autocomplete items={items} disabled>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
inputGroupProps={{ className: "w-fit" }}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}With label
"use client";
import { useId } from "react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
export function AutocompleteDemo() {
const id = useId();
return (
<Autocomplete items={items}>
<div className="flex flex-col items-start gap-2">
<label htmlFor={id}>Fruits</label>
<AutocompleteInput aria-label="Search items" id={id} placeholder="Search items…" />
</div>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}Inline autocomplete
Controls how the autocomplete behaves with respect to list filtering and inline autocompletion.
list(default): items are dynamically filtered based on the input value. The input value does not change based on the active item.both: items are dynamically filtered based on the input value, which will temporarily change based on the active item (inline autocompletion).inline: items are static (not filtered), and the input value will temporarily change based on the active item (inline autocompletion).none: items are static (not filtered), and the input value will not change based on the active item.
"use client";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
export function AutocompleteDemo() {
return (
<Autocomplete items={items} mode="both">
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
inputGroupProps={{ className: "max-w-64" }}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}Auto highlight
Whether the first matching item is highlighted automatically.
true- Highlight after the user types and keep the highlight while the query changes.always- Always highlight the first item.
"use client";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
export function AutocompleteDemo() {
return (
<Autocomplete items={items} autoHighlight>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
inputGroupProps={{ className: "max-w-64" }}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}With clear button
Pass showClear prop to the AutocompleteInput component to show a clear button.
"use client";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
export function AutocompleteDemo() {
return (
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
showClear
inputGroupProps={{ className: "w-64" }}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}With clear & trigger button
Pass showClear and showTrigger props to the AutocompleteInput component to show both buttons.
"use client";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
export function AutocompleteDemo() {
return (
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
showTrigger
showClear
inputGroupProps={{ className: "w-64" }}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}With start addon
Use the startAddon prop to add an icon or any other element before the input.
"use client";
import { IconSearch } from "@tabler/icons-react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
export function AutocompleteDemo() {
return (
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
startAddon={<IconSearch />}
inputGroupProps={{ className: "w-64" }}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}Grouped
"use client";
import {
Autocomplete,
AutocompleteCollection,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
groupAutocompleteItems,
} from "@/components/ui/autocomplete";
type Item = {
label: string;
value: string;
group: string;
};
const items: Item[] = [
{ label: "React", value: "react", group: "Frontend" },
{ label: "Vue", value: "vue", group: "Frontend" },
{ label: "Angular", value: "angular", group: "Frontend" },
{ label: "Svelte", value: "svelte", group: "Frontend" },
{ label: "Next.js", value: "nextjs", group: "Frontend" },
{ label: "Nuxt.js", value: "nuxtjs", group: "Frontend" },
{ label: "Node.js", value: "nodejs", group: "Backend" },
{ label: "Django", value: "django", group: "Backend" },
{ label: "Flask", value: "flask", group: "Backend" },
{ label: "Ruby on Rails", value: "rails", group: "Backend" },
{ label: "Spring Boot", value: "springboot", group: "Backend" },
{ label: "Flutter", value: "flutter", group: "Mobile" },
{ label: "React Native", value: "reactnative", group: "Mobile" },
{ label: "SwiftUI", value: "swiftui", group: "Mobile" },
{ label: "Kotlin Multiplatform", value: "kotlinmultiplatform", group: "Mobile" },
];
const groupedItems = groupAutocompleteItems(items);
type GroupItem = (typeof groupedItems)[number];
export function AutocompleteDemo() {
return (
<Autocomplete items={groupedItems}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search frameworks..."
inputGroupProps={{ className: "w-fit" }}
showTrigger
showClear
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(group: GroupItem) => (
<AutocompleteGroup key={group.value} items={group.items}>
<AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
<AutocompleteCollection>
{(item: Item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteCollection>
</AutocompleteGroup>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}Limit results
"use client";
import { useMemo, useState } from "react";
import { IconSearch } from "@tabler/icons-react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
AutocompleteStatus,
useAutocompleteFilter,
} from "@/components/ui/autocomplete";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Watermelon", value: "watermelon" },
];
const limit = 5;
export function AutocompleteDemo() {
const [value, setValue] = useState("");
const { contains } = useAutocompleteFilter({ sensitivity: "base" });
const totalMatches = useMemo(() => {
const trimmed = value.trim();
if (!trimmed) {
return items.length;
}
return items.filter((t) => contains(t.value, trimmed)).length;
}, [value, contains]);
const moreCount = Math.max(0, totalMatches - limit);
return (
<Autocomplete items={items} limit={limit} value={value} onValueChange={setValue}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
startAddon={<IconSearch />}
inputGroupProps={{ className: "w-64" }}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
{moreCount > 0 && (
<AutocompleteStatus>+{moreCount} more (keep typing to narrow down)</AutocompleteStatus>
)}
</AutocompletePopup>
</Autocomplete>
);
}Async search
"use client";
import { useEffect, useState } from "react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
AutocompleteStatus,
useAutocompleteFilter,
} from "@/components/ui/autocomplete";
import { Spinner } from "@/components/ui/spinner";
interface Movie {
id: string;
title: string;
year: number;
}
const top20Movies: Movie[] = [
{ id: "1", title: "The Shawshank Redemption", year: 1994 },
{ id: "2", title: "The Godfather", year: 1972 },
{ id: "3", title: "The Dark Knight", year: 2008 },
{ id: "4", title: "The Godfather Part II", year: 1974 },
{ id: "5", title: "12 Angry Men", year: 1957 },
{ id: "6", title: "The Lord of the Rings: The Return of the King", year: 2003 },
{ id: "7", title: "Schindler's List", year: 1993 },
{ id: "8", title: "Pulp Fiction", year: 1994 },
{ id: "9", title: "The Lord of the Rings: The Fellowship of the Ring", year: 2001 },
{ id: "10", title: "The Good, the Bad and the Ugly", year: 1966 },
{ id: "11", title: "Forrest Gump", year: 1994 },
{ id: "12", title: "The Lord of the Rings: The Two Towers", year: 2002 },
{ id: "13", title: "Fight Club", year: 1999 },
{ id: "14", title: "Inception", year: 2010 },
{ id: "15", title: "Star Wars: Episode V – The Empire Strikes Back", year: 1980 },
{ id: "16", title: "The Matrix", year: 1999 },
{ id: "17", title: "Goodfellas", year: 1990 },
{ id: "18", title: "Interstellar", year: 2014 },
{ id: "19", title: "One Flew Over the Cuckoo's Nest", year: 1975 },
{ id: "20", title: "Se7en", year: 1995 },
];
async function searchMovies(
query: string,
filter: (item: string, query: string) => boolean
): Promise<Movie[]> {
await new Promise((resolve) => setTimeout(resolve, Math.random() * 500 + 100));
if (Math.random() < 0.01 || query === "will_error") {
throw new Error("Network error");
}
return top20Movies.filter(
(movie) => filter(movie.title, query) || filter(movie.year.toString(), query)
);
}
export function AutocompleteDemo() {
const [searchValue, setSearchValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [searchResults, setSearchResults] = useState<Movie[]>([]);
const [error, setError] = useState<string | null>(null);
const { contains } = useAutocompleteFilter();
useEffect(() => {
if (!searchValue) {
setSearchResults([]);
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
let ignore = false;
const timeoutId = setTimeout(async () => {
try {
const results = await searchMovies(searchValue, contains);
if (!ignore) setSearchResults(results);
} catch {
if (!ignore) {
setError("Failed to fetch movies. Please try again.");
setSearchResults([]);
}
} finally {
if (!ignore) setIsLoading(false);
}
}, 300);
return () => {
clearTimeout(timeoutId);
ignore = true;
};
}, [searchValue, contains]);
return (
<Autocomplete
items={searchResults}
itemToStringValue={(item: unknown) => (item as Movie).title}
value={searchValue}
onValueChange={setSearchValue}
>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
inputGroupProps={{ className: "w-64" }}
/>
<AutocompletePopup>
{isLoading || (
<AutocompleteEmpty>
Movie or year "{searchValue}" does not exist in the Top 20 movies
</AutocompleteEmpty>
)}
<AutocompleteStatus className="border-b">
{isLoading ? (
<span className="flex items-center justify-between gap-2 text-muted-foreground">
Searching....
<Spinner className="sm:size-4" />
</span>
) : (
<span>
{searchResults.length} result{searchResults.length === 1 ? "" : "s"} found
</span>
)}
{error && <span className="text-danger">{error}</span>}
</AutocompleteStatus>
<AutocompleteList scrollArea={true}>
{(movie) => (
<AutocompleteItem key={movie.id} value={movie}>
<div className="flex w-full flex-col gap-1">
<div className="font-medium">{movie.title}</div>
<div className="text-muted-foreground text-xs">{movie.year}</div>
</div>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}API Reference
Autocomplete Input props
Component type props
Prop
Type
Default
Expand prop details
The size of the button.
Type
'xs' | 'sm' | 'default' | 'lg' | 'xl' | 'icon-sm' | 'icon' | 'icon-lg' | 'icon-xl'Default
'default'Whether to show the trigger button.
Type
booleanDefault
falseWhether to show the clear button.
Type
booleanDefault
falseAn element to display before the input.
Type
React.ReactNodeProps to pass to the trigger button.
Type
React.ComponentProps<typeof AutocompleteTrigger>Props to pass to the clear button.
Type
React.ComponentProps<typeof AutocompleteClear>Autocomplete Positioner props
Component type props
Prop
Type
Default
Expand prop details
The distance in pixels from the trigger to the popup.
Type
numberDefault
4Autocomplete Popup props
Component type props
Prop
Type
Default
Expand prop details
Props to pass to the portal.
Type
React.ComponentProps<typeof AutocompletePortal>Props to pass to the positioner.
Type
React.ComponentProps<typeof AutocompletePositioner>Autocomplete List props
Component type props
Prop
Type
Default
Expand prop details
Whether to wrap the list in a scroll area. If false, native scrollbar will be shown if the list exceeds the max height.
Type
booleanDefault
true