Autocomplete
An input that suggests options as you type. Built with Base UI and Tailwind CSS. Copy-paste ready.
"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" },
];
export default function Particle() {
return (
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/autocomplete
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>API Reference
The AutocompleteInput component extends the original Base UI component with a few extra props:
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "lg" | "default" | The size variant of the input field. |
showTrigger | boolean | false | Whether to display a trigger button (chevron icon) on the right side of the input. |
showClear | boolean | false | Whether to display a clear button (X icon) on the right side of the input when there is a value. |
Examples
Sizes
Renders the sm, default, and lg size variants to show how the autocomplete input scales across layout contexts.
"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" },
];
export default function Particle() {
return (
<Autocomplete disabled items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}
With Label
Associates a Label with the autocomplete input for accessible identification without a Field wrapper.
"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" },
];
export default function Particle() {
return (
<>
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
size="sm"
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
size="lg"
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
</>
);
}
Inline Autocomplete
Autofill the input with the highlighted item while navigating with arrow keys.
"use client";
import { useId } from "react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
import { Label } from "@/components/ui/label";
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" },
];
export default function Particle() {
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>
);
}
Auto Highlight
Sets mode="both" so the input filters the list while also completing the typed text inline, accepting the suggestion with Enter.
"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" },
];
export default function Particle() {
return (
<Autocomplete items={items} mode="both">
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}
Trigger and Clear
Enables both showTrigger and showClear on AutocompleteInput to add a chevron toggle and an X clear button to the field.
"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" },
];
export default function Particle() {
return (
<Autocomplete autoHighlight items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}
With Clear Button
Enables showClear on AutocompleteInput to display an X button that resets the value when the field is non-empty.
"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" },
];
export default function Particle() {
return (
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
showClear
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}
With Search Icon
Prepends a search icon inside the input to visually reinforce the filtering behavior of the autocomplete field.
"use client";
import { SearchIcon } from "lucide-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" },
];
export default function Particle() {
return (
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
startAddon={<SearchIcon />}
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}
Grouped Items
Organizes options into labeled sections using AutocompleteGroup for categorized lists like countries by region.
"use client";
import { Fragment } from "react";
import {
Autocomplete,
AutocompleteCollection,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
AutocompleteSeparator,
} from "@/components/ui/autocomplete";
// Grouped items demo
type Tag = { id: string; label: string; group: "Status" | "Priority" | "Team" };
type TagGroup = { value: string; items: Tag[] };
const tagsData: Tag[] = [
// Status
{ group: "Status", id: "s-open", label: "Open" },
{ group: "Status", id: "s-in-progress", label: "In progress" },
{ group: "Status", id: "s-blocked", label: "Blocked" },
{ group: "Status", id: "s-resolved", label: "Resolved" },
{ group: "Status", id: "s-closed", label: "Closed" },
// Priority
{ group: "Priority", id: "p-low", label: "Low" },
{ group: "Priority", id: "p-medium", label: "Medium" },
{ group: "Priority", id: "p-high", label: "High" },
{ group: "Priority", id: "p-urgent", label: "Urgent" },
// Team
{ group: "Team", id: "t-design", label: "Design" },
{ group: "Team", id: "t-frontend", label: "Frontend" },
{ group: "Team", id: "t-backend", label: "Backend" },
{ group: "Team", id: "t-devops", label: "DevOps" },
{ group: "Team", id: "t-qa", label: "QA" },
{ group: "Team", id: "t-mobile", label: "Mobile" },
{ group: "Team", id: "t-data", label: "Data" },
{ group: "Team", id: "t-security", label: "Security" },
{ group: "Team", id: "t-platform", label: "Platform" },
{ group: "Team", id: "t-infra", label: "Infrastructure" },
{ group: "Team", id: "t-product", label: "Product" },
{ group: "Team", id: "t-marketing", label: "Marketing" },
{ group: "Team", id: "t-sales", label: "Sales" },
{ group: "Team", id: "t-support", label: "Support" },
{ group: "Team", id: "t-research", label: "Research" },
{ group: "Team", id: "t-content", label: "Content" },
{ group: "Team", id: "t-analytics", label: "Analytics" },
{ group: "Team", id: "t-operations", label: "Operations" },
{ group: "Team", id: "t-finance", label: "Finance" },
{ group: "Team", id: "t-hr", label: "HR" },
{ group: "Team", id: "t-legal", label: "Legal" },
{ group: "Team", id: "t-growth", label: "Growth" },
{ group: "Team", id: "t-partner", label: "Partner" },
{ group: "Team", id: "t-community", label: "Community" },
{ group: "Team", id: "t-docs", label: "Docs" },
{ group: "Team", id: "t-l10n", label: "Localization" },
{ group: "Team", id: "t-a11y", label: "Accessibility" },
{ group: "Team", id: "t-sre", label: "SRE" },
{ group: "Team", id: "t-release", label: "Release" },
{ group: "Team", id: "t-architecture", label: "Architecture" },
{ group: "Team", id: "t-ux", label: "UX" },
{ group: "Team", id: "t-ui", label: "UI" },
{ group: "Team", id: "t-management", label: "Management" },
];
function groupTags(tags: Tag[]): TagGroup[] {
const groups: Record<string, Tag[]> = {};
for (const tag of tags) {
if (!groups[tag.group]) {
groups[tag.group] = [];
}
groups[tag.group]?.push(tag);
}
const order: Array<TagGroup["value"]> = ["Status", "Priority", "Team"];
return order.map((value) => ({ items: groups[value] ?? [], value }));
}
const groupedTags: TagGroup[] = groupTags(tagsData);
export default function Particle() {
return (
<Autocomplete items={groupedTags}>
<div className="flex flex-col items-start gap-2">
<AutocompleteInput
aria-label="Search tags"
placeholder="e.g. feature"
/>
</div>
<AutocompletePopup>
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(group: TagGroup) => (
<Fragment key={group.value}>
<AutocompleteGroup items={group.items}>
<AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
<AutocompleteCollection>
{(tag: Tag) => (
<AutocompleteItem key={tag.id} value={tag}>
{tag.label}
</AutocompleteItem>
)}
</AutocompleteCollection>
</AutocompleteGroup>
{group.value !== "Team" && <AutocompleteSeparator />}
</Fragment>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
);
}
Limit Results
Caps visible suggestions and shows a count of hidden matches.
"use client";
import { useMemo, useState } from "react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
AutocompleteStatus,
useAutocompleteFilter,
} from "@/components/ui/autocomplete";
// Limit results demo
const limit = 7;
type SimpleTag = { id: string; value: string };
const manyTags: SimpleTag[] = [
{ id: "lang-js", value: "JavaScript" },
{ id: "lang-ts", value: "TypeScript" },
{ id: "lang-py", value: "Python" },
{ id: "lang-java", value: "Java" },
{ id: "lang-csharp", value: "C#" },
{ id: "lang-cpp", value: "C++" },
{ id: "lang-c", value: "C" },
{ id: "lang-go", value: "Go" },
{ id: "lang-rust", value: "Rust" },
{ id: "lang-rb", value: "Ruby" },
{ id: "lang-php", value: "PHP" },
{ id: "lang-swift", value: "Swift" },
{ id: "lang-kotlin", value: "Kotlin" },
{ id: "lang-scala", value: "Scala" },
{ id: "lang-elixir", value: "Elixir" },
{ id: "lang-hs", value: "Haskell" },
{ id: "lang-dart", value: "Dart" },
{ id: "lang-objc", value: "Objective-C" },
{ id: "lang-julia", value: "Julia" },
{ id: "lang-r", value: "R" },
{ id: "lang-perl", value: "Perl" },
{ id: "lang-lua", value: "Lua" },
{ id: "lang-ocaml", value: "OCaml" },
{ id: "lang-fsharp", value: "F#" },
];
export default function Particle() {
const [value, setValue] = useState("");
const { contains } = useAutocompleteFilter({ sensitivity: "base" });
const totalMatches = useMemo(() => {
const trimmed = value.trim();
if (!trimmed) return manyTags.length;
return manyTags.filter((t) => contains(t.value, trimmed)).length;
}, [value, contains]);
const moreCount = Math.max(0, totalMatches - limit);
return (
<Autocomplete
items={manyTags}
limit={limit}
onValueChange={setValue}
value={value}
>
<AutocompleteInput placeholder="e.g. feature" />
<AutocompletePopup>
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(tag: SimpleTag) => (
<AutocompleteItem key={tag.id} value={tag}>
{tag.value}
</AutocompleteItem>
)}
</AutocompleteList>
{moreCount > 0 && (
<AutocompleteStatus>
+{moreCount} more (keep typing to narrow down)
</AutocompleteStatus>
)}
</AutocompletePopup>
</Autocomplete>
);
}
Async Search
Debounced remote search with loading and error states.
"use client";
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import {
Autocomplete,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
AutocompleteStatus,
useAutocompleteFilter,
} from "@/components/ui/autocomplete";
import { Spinner } from "@/components/ui/spinner";
type Movie = { id: string; title: string; year: number };
const top100Movies: 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: "8", title: "Pulp Fiction", year: 1994 },
{ id: "11", title: "Forrest Gump", year: 1994 },
{ id: "14", title: "Inception", year: 2010 },
];
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 top100Movies.filter(
(movie) =>
filter(movie.title, query) || filter(movie.year.toString(), query),
);
}
export default function Particle() {
const [searchValue, setSearchValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [searchResults, setSearchResults] = useState<Movie[]>([]);
const [error, setError] = useState<string | null>(null);
const { contains } = useAutocompleteFilter({ sensitivity: "base" });
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]);
let status: ReactNode = `${searchResults.length} result${searchResults.length === 1 ? "" : "s"} found`;
if (isLoading) {
status = (
<span className="flex items-center justify-between gap-2 text-muted-foreground">
Searching...
<Spinner className="size-4.5 sm:size-4" />
</span>
);
} else if (error) {
status = (
<span className="font-normal text-destructive text-sm">{error}</span>
);
} else if (searchResults.length === 0 && searchValue) {
status = (
<span className="font-normal text-muted-foreground text-sm">
Movie or year "{searchValue}" does not exist in the Top 100 IMDb movies
</span>
);
}
const shouldRenderPopup = searchValue !== "";
return (
<Autocomplete
filter={null}
items={searchResults}
itemToStringValue={(item: unknown) => (item as Movie).title}
onValueChange={setSearchValue}
value={searchValue}
>
<AutocompleteInput placeholder="e.g. Pulp Fiction or 1994" />
{shouldRenderPopup && (
<AutocompletePopup aria-busy={isLoading || undefined}>
<AutocompleteStatus className="text-muted-foreground">
{status}
</AutocompleteStatus>
<AutocompleteList>
{(movie: 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>
);
}
Form Integration
Wraps the autocomplete in Form and Field for required validation and a visible FieldError on submit.
"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
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" },
];
export default function Particle() {
const [loading, setLoading] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const selectedItem = formData.get("item");
// Base UI extracts the 'label' property from objects, so we need to find the corresponding value
const itemValue =
items.find((item) => item.label === selectedItem)?.value || selectedItem;
setLoading(true);
await new Promise((r) => setTimeout(r, 800));
setLoading(false);
alert(`Favorite item: ${itemValue || ""}`);
};
return (
<Form className="flex w-full max-w-64 flex-col gap-4" onSubmit={onSubmit}>
<Field name="item">
<FieldLabel>Favorite item</FieldLabel>
<Autocomplete items={items} required>
<AutocompleteInput placeholder="Search items…" />
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
<FieldError>Please select a item.</FieldError>
</Field>
<Button loading={loading} type="submit">
Submit
</Button>
</Form>
);
}
Timezone Picker
Grouped by region with a monospace UTC offset badge on each item.
//biome-ignore-all lint/suspicious/noAssignInExpressions:<>
"use client";
import { Globe } from "lucide-react";
import { Fragment } from "react";
import {
Autocomplete,
AutocompleteCollection,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
AutocompleteSeparator,
} from "@/components/ui/autocomplete";
type Timezone = {
id: string;
label: string;
city: string;
offset: string;
region: "Americas" | "Europe & Africa" | "Asia & Pacific";
};
type RegionGroup = { value: string; items: Timezone[] };
const timezones: Timezone[] = [
{
city: "Los Angeles",
id: "pst",
label: "Pacific Time",
offset: "UTC−8",
region: "Americas",
},
{
city: "Denver",
id: "mst",
label: "Mountain Time",
offset: "UTC−7",
region: "Americas",
},
{
city: "Chicago",
id: "cst",
label: "Central Time",
offset: "UTC−6",
region: "Americas",
},
{
city: "New York",
id: "est",
label: "Eastern Time",
offset: "UTC−5",
region: "Americas",
},
{
city: "Buenos Aires",
id: "art",
label: "Argentina Time",
offset: "UTC−3",
region: "Americas",
},
{
city: "London",
id: "gmt",
label: "Greenwich Mean Time",
offset: "UTC±0",
region: "Europe & Africa",
},
{
city: "Paris",
id: "cet",
label: "Central European Time",
offset: "UTC+1",
region: "Europe & Africa",
},
{
city: "Helsinki",
id: "eet",
label: "Eastern European Time",
offset: "UTC+2",
region: "Europe & Africa",
},
{
city: "Moscow",
id: "msk",
label: "Moscow Time",
offset: "UTC+3",
region: "Europe & Africa",
},
{
city: "Nairobi",
id: "eat",
label: "East Africa Time",
offset: "UTC+3",
region: "Europe & Africa",
},
{
city: "Dubai",
id: "gst",
label: "Gulf Standard Time",
offset: "UTC+4",
region: "Europe & Africa",
},
{
city: "Mumbai",
id: "ist",
label: "India Standard Time",
offset: "UTC+5:30",
region: "Asia & Pacific",
},
{
city: "Bangkok",
id: "ict",
label: "Indochina Time",
offset: "UTC+7",
region: "Asia & Pacific",
},
{
city: "Singapore",
id: "sgt",
label: "Singapore Time",
offset: "UTC+8",
region: "Asia & Pacific",
},
{
city: "Tokyo",
id: "jst",
label: "Japan Standard Time",
offset: "UTC+9",
region: "Asia & Pacific",
},
{
city: "Sydney",
id: "aest",
label: "AUS Eastern Time",
offset: "UTC+10",
region: "Asia & Pacific",
},
{
city: "Auckland",
id: "nzst",
label: "New Zealand Time",
offset: "UTC+12",
region: "Asia & Pacific",
},
];
function groupByRegion(tzs: Timezone[]): RegionGroup[] {
const map: Record<string, Timezone[]> = {};
for (const tz of tzs) {
(map[tz.region] ??= []).push(tz);
}
const order: Timezone["region"][] = [
"Americas",
"Europe & Africa",
"Asia & Pacific",
];
return order.map((r) => ({ items: map[r] ?? [], value: r }));
}
const regionGroups = groupByRegion(timezones);
export default function Particle() {
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<label className="font-medium text-sm">Timezone</label>
<Autocomplete items={regionGroups}>
<AutocompleteInput
placeholder="Select a timezone…"
showClear
showTrigger
startAddon={<Globe className="size-4 text-muted-foreground" />}
/>
<AutocompletePopup>
<AutocompleteEmpty>No timezone found.</AutocompleteEmpty>
<AutocompleteList>
{(group: RegionGroup) => (
<Fragment key={group.value}>
<AutocompleteGroup items={group.items}>
<AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
<AutocompleteCollection>
{(tz: Timezone) => (
<AutocompleteItem key={tz.id} value={tz}>
<div className="flex w-full items-center gap-2">
<span className="flex-1 truncate">{tz.label}</span>
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground">
{tz.offset}
</span>
</div>
</AutocompleteItem>
)}
</AutocompleteCollection>
</AutocompleteGroup>
{group.value !== "Asia & Pacific" && <AutocompleteSeparator />}
</Fragment>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
</div>
);
}
Person Picker
Rich assignee selector with avatar initials, email, and role badge.
"use client";
import { User } from "lucide-react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
type TeamMember = {
id: string;
label: string;
email: string;
role: string;
avatarColor: string;
initials: string;
};
const teamMembers: TeamMember[] = [
{
avatarColor: "#8b5cf6",
email: "alice@company.com",
id: "1",
initials: "AJ",
label: "Alice Johnson",
role: "Design",
},
{
avatarColor: "#3b82f6",
email: "bob@company.com",
id: "2",
initials: "BC",
label: "Bob Chen",
role: "Frontend",
},
{
avatarColor: "#10b981",
email: "carol@company.com",
id: "3",
initials: "CD",
label: "Carol Davis",
role: "Backend",
},
{
avatarColor: "#f97316",
email: "dan@company.com",
id: "4",
initials: "DP",
label: "Dan Park",
role: "DevOps",
},
{
avatarColor: "#ec4899",
email: "eva@company.com",
id: "5",
initials: "EM",
label: "Eva Martín",
role: "Product",
},
{
avatarColor: "#eab308",
email: "frank@company.com",
id: "6",
initials: "FW",
label: "Frank Wright",
role: "QA",
},
{
avatarColor: "#06b6d4",
email: "grace@company.com",
id: "7",
initials: "GL",
label: "Grace Lee",
role: "Mobile",
},
{
avatarColor: "#ef4444",
email: "hiro@company.com",
id: "8",
initials: "HT",
label: "Hiro Tanaka",
role: "Data",
},
];
export default function Particle() {
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<label className="font-medium text-sm">Assign to</label>
<Autocomplete items={teamMembers}>
<AutocompleteInput
placeholder="Search team members…"
showClear
showTrigger
startAddon={<User className="size-4 text-muted-foreground" />}
/>
<AutocompletePopup>
<AutocompleteEmpty>No team members found.</AutocompleteEmpty>
<AutocompleteList>
{(member: TeamMember) => (
<AutocompleteItem key={member.id} value={member}>
<div className="flex w-full items-center gap-2.5">
<span
aria-hidden="true"
className="inline-flex size-7 shrink-0 items-center justify-center rounded-full font-semibold text-white text-xs"
style={{ backgroundColor: member.avatarColor }}
>
{member.initials}
</span>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-sm leading-tight">
{member.label}
</div>
<div className="truncate text-muted-foreground text-xs leading-tight">
{member.email}
</div>
</div>
<span className="shrink-0 rounded-full border px-2 py-0.5 text-muted-foreground text-xs">
{member.role}
</span>
</div>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
</div>
);
}
Country Picker
Each item displays a flag emoji, full country name, and the ISO 3166-1 alpha-2 code aligned to the trailing edge. Includes a clear and trigger button.
"use client";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
type Country = { id: string; label: string; code: string; flag: string };
const countries: Country[] = [
{ code: "US", flag: "🇺🇸", id: "us", label: "United States" },
{ code: "GB", flag: "🇬🇧", id: "gb", label: "United Kingdom" },
{ code: "DE", flag: "🇩🇪", id: "de", label: "Germany" },
{ code: "FR", flag: "🇫🇷", id: "fr", label: "France" },
{ code: "JP", flag: "🇯🇵", id: "jp", label: "Japan" },
{ code: "IN", flag: "🇮🇳", id: "in", label: "India" },
{ code: "CA", flag: "🇨🇦", id: "ca", label: "Canada" },
{ code: "AU", flag: "🇦🇺", id: "au", label: "Australia" },
{ code: "BR", flag: "🇧🇷", id: "br", label: "Brazil" },
{ code: "SG", flag: "🇸🇬", id: "sg", label: "Singapore" },
{ code: "NL", flag: "🇳🇱", id: "nl", label: "Netherlands" },
{ code: "SE", flag: "🇸🇪", id: "se", label: "Sweden" },
{ code: "NO", flag: "🇳🇴", id: "no", label: "Norway" },
{ code: "CH", flag: "🇨🇭", id: "ch", label: "Switzerland" },
{ code: "KR", flag: "🇰🇷", id: "kr", label: "South Korea" },
{ code: "MX", flag: "🇲🇽", id: "mx", label: "Mexico" },
{ code: "ES", flag: "🇪🇸", id: "es", label: "Spain" },
{ code: "IT", flag: "🇮🇹", id: "it", label: "Italy" },
{ code: "PL", flag: "🇵🇱", id: "pl", label: "Poland" },
{ code: "ZA", flag: "🇿🇦", id: "za", label: "South Africa" },
];
export default function Particle() {
return (
<div className="flex w-full max-w-xs flex-col gap-1.5">
<label className="font-medium text-sm">Country</label>
<Autocomplete items={countries}>
<AutocompleteInput
placeholder="Search country…"
showClear
showTrigger
/>
<AutocompletePopup>
<AutocompleteEmpty>No country found.</AutocompleteEmpty>
<AutocompleteList>
{(country: Country) => (
<AutocompleteItem key={country.id} value={country}>
<div className="flex w-full items-center gap-2.5">
<span aria-hidden="true" className="text-base leading-none">
{country.flag}
</span>
<span className="flex-1 font-medium text-sm">
{country.label}
</span>
<span className="font-mono text-muted-foreground text-xs">
{country.code}
</span>
</div>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
</div>
);
}
Repository Picker
GitHub-style repository search showing the org/name in monospace, a language color dot, and a star count. Useful for linking issues or PRs to a repo.
"use client";
import { StarIcon } from "lucide-react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
type Repo = {
id: string;
label: string;
org: string;
name: string;
language: string;
languageColor: string;
stars: string;
description: string;
};
const repos: Repo[] = [
{
description: "A JavaScript library for building user interfaces",
id: "react",
label: "facebook/react",
language: "JavaScript",
languageColor: "#f1e05a",
name: "react",
org: "facebook",
stars: "228k",
},
{
description: "The React Framework for the Web",
id: "nextjs",
label: "vercel/next.js",
language: "TypeScript",
languageColor: "#3178c6",
name: "next.js",
org: "vercel",
stars: "127k",
},
{
description: "Build faster websites with Astro",
id: "astro",
label: "withastro/astro",
language: "TypeScript",
languageColor: "#3178c6",
name: "astro",
org: "withastro",
stars: "47k",
},
{
description: "Rapidly build modern websites without leaving your HTML",
id: "tailwind",
label: "tailwindlabs/tailwindcss",
language: "CSS",
languageColor: "#563d7c",
name: "tailwindcss",
org: "tailwindlabs",
stars: "83k",
},
{
description: "Next-generation ORM for Node.js & TypeScript",
id: "prisma",
label: "prisma/prisma",
language: "TypeScript",
languageColor: "#3178c6",
name: "prisma",
org: "prisma",
stars: "39k",
},
{
description: "An extremely fast JavaScript runtime",
id: "bun",
label: "oven-sh/bun",
language: "Zig",
languageColor: "#ec915c",
name: "bun",
org: "oven-sh",
stars: "73k",
},
{
description: "A fast all-in-one JavaScript runtime",
id: "deno",
label: "denoland/deno",
language: "Rust",
languageColor: "#dea584",
name: "deno",
org: "denoland",
stars: "97k",
},
];
export default function Particle() {
return (
<div className="flex w-full max-w-sm flex-col gap-1.5">
<label className="font-medium text-sm">Link repository</label>
<Autocomplete items={repos}>
<AutocompleteInput placeholder="Search repositories…" showClear />
<AutocompletePopup>
<AutocompleteEmpty>No repositories found.</AutocompleteEmpty>
<AutocompleteList>
{(repo: Repo) => (
<AutocompleteItem key={repo.id} value={repo}>
<div className="flex w-full flex-col gap-0.5 py-0.5">
<div className="flex items-center justify-between gap-2">
<span className="font-medium font-mono text-sm">
<span className="text-muted-foreground">{repo.org}/</span>
{repo.name}
</span>
<span className="flex shrink-0 items-center gap-1 text-muted-foreground text-xs">
<StarIcon className="size-3" />
{repo.stars}
</span>
</div>
<div className="flex items-center gap-2">
<span
aria-hidden="true"
className="size-2.5 shrink-0 rounded-full"
style={{ backgroundColor: repo.languageColor }}
/>
<span className="text-muted-foreground text-xs">
{repo.language}
</span>
<span className="truncate text-muted-foreground text-xs">
· {repo.description}
</span>
</div>
</div>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
</div>
);
}
Multi-select Tag Input
Selected items are displayed as dismissible Badge chips above the input. Choosing an item adds it to the chip list and clears the field; clicking the chip's × removes it.
"use client";
import { XIcon } from "lucide-react";
import { useState } from "react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
type Tech = { id: string; label: string };
const allTechs: Tech[] = [
{ id: "react", label: "React" },
{ id: "vue", label: "Vue" },
{ id: "angular", label: "Angular" },
{ id: "svelte", label: "Svelte" },
{ id: "nextjs", label: "Next.js" },
{ id: "nuxt", label: "Nuxt" },
{ id: "remix", label: "Remix" },
{ id: "astro", label: "Astro" },
{ id: "typescript", label: "TypeScript" },
{ id: "graphql", label: "GraphQL" },
{ id: "tailwind", label: "Tailwind CSS" },
{ id: "prisma", label: "Prisma" },
];
export function Pattern() {
const [selected, setSelected] = useState<Tech[]>([
{ id: "react", label: "React" },
{ id: "typescript", label: "TypeScript" },
]);
const [inputValue, setInputValue] = useState("");
const available = allTechs.filter(
(t) => !selected.some((s) => s.id === t.id),
);
const handleValueChange = (value: string) => {
setInputValue(value);
const match = allTechs.find(
(t) => t.label.toLowerCase() === value.toLowerCase(),
);
if (match && !selected.some((s) => s.id === match.id)) {
setSelected((prev) => [...prev, match]);
setInputValue("");
}
};
const remove = (id: string) =>
setSelected((prev) => prev.filter((t) => t.id !== id));
return (
<div className="flex w-full max-w-sm flex-col gap-2">
<Label>Tech stack</Label>
{selected.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{selected.map((tech) => (
<Badge className="gap-1 pr-1" key={tech.id} variant="secondary">
{tech.label}
<button
aria-label={`Remove ${tech.label}`}
className="rounded-sm opacity-60 hover:opacity-100"
onClick={() => remove(tech.id)}
type="button"
>
<XIcon className="size-3" />
</button>
</Badge>
))}
</div>
)}
<Autocomplete
items={available}
onValueChange={handleValueChange}
value={inputValue}
>
<AutocompleteInput placeholder="Add technology…" showClear />
<AutocompletePopup>
<AutocompleteEmpty>All technologies selected.</AutocompleteEmpty>
<AutocompleteList>
{(tech: Tech) => (
<AutocompleteItem key={tech.id} value={tech}>
{tech.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
</div>
);
}
Priority Selector
Each option renders a colored icon paired with a label and a one-line description explaining when to use that priority level.
"use client";
import {
AlertCircleIcon,
ArrowDownIcon,
ArrowRightIcon,
ArrowUpIcon,
MinusIcon,
} from "lucide-react";
import type { ElementType } from "react";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
type Priority = {
id: string;
label: string;
description: string;
colorClass: string;
icon: ElementType;
};
const priorities: Priority[] = [
{
colorClass: "text-red-500",
description: "Requires immediate attention",
icon: AlertCircleIcon,
id: "urgent",
label: "Urgent",
},
{
colorClass: "text-orange-500",
description: "Should be done soon",
icon: ArrowUpIcon,
id: "high",
label: "High",
},
{
colorClass: "text-yellow-500",
description: "Normal workflow priority",
icon: ArrowRightIcon,
id: "medium",
label: "Medium",
},
{
colorClass: "text-slate-400",
description: "Address when time allows",
icon: ArrowDownIcon,
id: "low",
label: "Low",
},
{
colorClass: "text-muted-foreground",
description: "Not yet prioritized",
icon: MinusIcon,
id: "none",
label: "No priority",
},
];
export default function Particle() {
return (
<div className="flex w-full max-w-xs flex-col gap-1.5">
<label className="font-medium text-sm">Priority</label>
<Autocomplete items={priorities}>
<AutocompleteInput placeholder="Set priority…" showClear showTrigger />
<AutocompletePopup>
<AutocompleteEmpty>No priority found.</AutocompleteEmpty>
<AutocompleteList>
{(p: Priority) => (
<AutocompleteItem key={p.id} value={p}>
<div className="flex items-center gap-2.5">
<p.icon className={`size-4 shrink-0 ${p.colorClass}`} />
<div className="flex flex-col">
<span className="font-medium text-sm">{p.label}</span>
<span className="text-muted-foreground text-xs">
{p.description}
</span>
</div>
</div>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
</div>
);
}
Skill Picker with Groups
Options are organized into three AutocompleteGroup sections (Languages, Frameworks, Tools) with labeled separators between them.
"use client";
import { Fragment } from "react";
import {
Autocomplete,
AutocompleteCollection,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
AutocompleteSeparator,
} from "@/components/ui/autocomplete";
type Skill = {
id: string;
label: string;
category: "Language" | "Framework" | "Tool";
};
type SkillGroup = { value: string; items: Skill[] };
const skills: Skill[] = [
{ category: "Language", id: "ts", label: "TypeScript" },
{ category: "Language", id: "rust", label: "Rust" },
{ category: "Language", id: "go", label: "Go" },
{ category: "Language", id: "python", label: "Python" },
{ category: "Language", id: "java", label: "Java" },
{ category: "Framework", id: "nextjs", label: "Next.js" },
{ category: "Framework", id: "remix", label: "Remix" },
{ category: "Framework", id: "fastapi", label: "FastAPI" },
{ category: "Framework", id: "spring", label: "Spring Boot" },
{ category: "Framework", id: "trpc", label: "tRPC" },
{ category: "Tool", id: "docker", label: "Docker" },
{ category: "Tool", id: "k8s", label: "Kubernetes" },
{ category: "Tool", id: "gha", label: "GitHub Actions" },
{ category: "Tool", id: "terraform", label: "Terraform" },
{ category: "Tool", id: "grafana", label: "Grafana" },
];
function groupSkills(data: Skill[]): SkillGroup[] {
const order: Array<Skill["category"]> = ["Language", "Framework", "Tool"];
return order.map((cat) => ({
items: data.filter((s) => s.category === cat),
value: cat,
}));
}
const grouped = groupSkills(skills);
export default function Particle() {
return (
<div className="flex w-full max-w-xs flex-col gap-1.5">
<label className="font-medium text-sm">Skill</label>
<Autocomplete items={grouped}>
<AutocompleteInput placeholder="Search skills…" showClear showTrigger />
<AutocompletePopup>
<AutocompleteEmpty>No skills found.</AutocompleteEmpty>
<AutocompleteList>
{(group: SkillGroup) => (
<Fragment key={group.value}>
<AutocompleteGroup items={group.items}>
<AutocompleteGroupLabel>
{group.value}s
</AutocompleteGroupLabel>
<AutocompleteCollection>
{(skill: Skill) => (
<AutocompleteItem key={skill.id} value={skill}>
{skill.label}
</AutocompleteItem>
)}
</AutocompleteCollection>
</AutocompleteGroup>
{group.value !== "Tool" && <AutocompleteSeparator />}
</Fragment>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
</div>
);
}
On This Page

