Autocomplete
A text input that provides real-time suggestions as the user types.
Overview
The Autocomplete component provides an interactive text input with real-time suggestions. Built on Base UI primitives, it supports filtering, custom rendering, grouping, async data loading, and multiple size variants.
Usage
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
} from "@tilt-legal/cubitt-components/primitives";<Autocomplete items={tags}>
<Label htmlFor="tags">Search tags</Label>
<AutocompleteInput id="tags" placeholder="e.g. feature" />
<AutocompleteContent>
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(tag) => (
<AutocompleteItem key={tag.id} value={tag.value}>
{tag.value}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>Examples
Auto Highlight
Automatically highlights the first matching item as you type.
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
} from "@tilt-legal/cubitt-components/primitives";
import { Label } from "@tilt-legal/cubitt-components/primitives";
<Autocomplete items={tags} autoHighlight>
<Label htmlFor="tags">Search tags</Label>
<AutocompleteInput id="tags" placeholder="e.g. feature" className="mt-2" />
<AutocompleteContent>
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(tag) => (
<AutocompleteItem key={tag.id} value={tag.value}>
{tag.value}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>;Clear Button
Add a clear button to quickly reset the input value.
import {
AutocompleteClear,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteWrapper,
} from "@tilt-legal/cubitt-components/primitives";
import { useState } from "react";
import { Autocomplete } from "@tilt-legal/cubitt-components/primitives";
import { Label } from "@tilt-legal/cubitt-components/primitives";
const [value, setValue] = useState<string>("");
const filteredItems = tags.filter((item) =>
item.value.toLowerCase().includes(value.toLowerCase())
);
<Autocomplete
value={value}
onValueChange={setValue}
items={filteredItems}
itemToStringValue={(item) => item.value}
>
<Label className="flex flex-col gap-2">
Search tags
<AutocompleteWrapper>
<AutocompleteInput placeholder="e.g. feature" />
{value && <AutocompleteClear />}
</AutocompleteWrapper>
</Label>
<AutocompleteContent>
<AutocompleteEmpty>No tags found.</AutocompleteEmpty>
<AutocompleteList>
{(tag) => (
<AutocompleteItem key={tag.id} value={tag}>
{tag.value}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>;Grouped Items
Organize suggestions into categorized groups with labels.
import {
Autocomplete,
AutocompleteClear,
AutocompleteCollection,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
InputWrapper,
} from "@tilt-legal/cubitt-components/primitives";
import { Avatar, AvatarFallback, AvatarImage } from "@tilt-legal/cubitt-components/primitives";
import { Label } from "@tilt-legal/cubitt-components/primitives";
const [value, setValue] = React.useState("");
const [open, setOpen] = React.useState(false);
<Autocomplete
items={filteredItems}
value={value}
onValueChange={setValue}
open={open}
onOpenChange={setOpen}
itemToStringValue={(item) => item.name}
filter={null}
>
<Label className="flex flex-col gap-2">
Search users
<InputWrapper>
<AutocompleteInput placeholder="e.g. John, Developer, Marketing" />
{value && <AutocompleteClear />}
</InputWrapper>
</Label>
{open && (
<AutocompleteContent className="pt-0">
{filteredItems.length === 0 ? (
<AutocompleteEmpty>No matching users found.</AutocompleteEmpty>
) : (
<AutocompleteList className="p-0">
{(group) => (
<AutocompleteGroup
key={group.group}
items={group.items}
className="py-0"
>
<AutocompleteGroupLabel className="sticky top-0 z-10 bg-background py-3">
{group.group}
</AutocompleteGroupLabel>
<AutocompleteCollection>
{(item) => (
<AutocompleteItem key={item.id} value={item}>
<Avatar className="size-9">
<AvatarImage src={item.avatar} alt={item.name} />
<AvatarFallback>
{item.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{item.name}</div>
<div className="text-sm text-muted-foreground truncate">
{item.position}
</div>
</div>
</AutocompleteItem>
)}
</AutocompleteCollection>
</AutocompleteGroup>
)}
</AutocompleteList>
)}
</AutocompleteContent>
)}
</Autocomplete>;Async Search
Load suggestions dynamically from an API with loading states.
import {
Autocomplete,
AutocompleteClear,
AutocompleteContent,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteStatus,
InputWrapper,
} from "@tilt-legal/cubitt-components/primitives";
import { Avatar, AvatarFallback, AvatarImage } from "@tilt-legal/cubitt-components/primitives";
import { Label } from "@tilt-legal/cubitt-components/primitives";
import {
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompleteValue,
Label,
} from "@tilt-legal/cubitt-components/primitives";
import {
Loader2 } from "@tilt-legal/cubitt-icons/ui/outline";
const [searchValue,
setSearchValue] = React.useState("");
const [isLoading,
setIsLoading] = React.useState(false);
const [searchResults,
setSearchResults] = React.useState([]);
React.useEffect(() => {
if (!searchValue) {
setSearchResults([]);
return;
}
setIsLoading(true);
const timeoutId = setTimeout(async () => {
const results = await fetchDevelopers(searchValue);
setSearchResults(results);
setIsLoading(false);
},
300);
return () => clearTimeout(timeoutId);
},
[searchValue]);
<Autocomplete
items={searchResults}
value={searchValue}
onValueChange={setSearchValue}
itemToStringValue={(item) => item.name}
filter={null}
>
<Label className="flex flex-col gap-2">
Search developers
<InputWrapper>
<AutocompleteInput placeholder="e.g. John Smith,
React,
San Francisco" />
{searchValue && <AutocompleteClear />}
</InputWrapper>
</Label>
{searchValue && (
<AutocompleteContent>
<AutocompleteStatus>
{isLoading ? (
<div className="flex items-center gap-2">
<Loader2 className="size-4 animate-spin" />
Searching...
</div>
) : (
`${searchResults.length} results found`
)}
</AutocompleteStatus>
<AutocompleteList>
{(developer) => (
<AutocompleteItem key={developer.id} value={developer}>
<Avatar className="size-9">
<AvatarImage src={developer.avatar} alt={developer.name} />
<AvatarFallback>
{developer.name
.split(" ")
.map((n) => n[0])
.join("")}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{developer.name}</div>
<div className="text-sm text-muted-foreground truncate">
{developer.role} • {developer.location}
</div>
</div>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
)}
</Autocomplete>;Fuzzy Matching
Advanced fuzzy search with highlighted matches using match-sorter.
import * as React from "react";
import { Autocomplete } from "@tilt-legal/cubitt-components/primitives";
import {
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
Label,
useAutocompleteFilter,
} from "@tilt-legal/cubitt-components/primitives";
import {
matchSorter } from "match-sorter";
export default function FuzzyMatchingExample() {
return (
<Autocomplete
items={fuzzyItems}
filter={fuzzyFilter}
itemToStringValue={(item) => item.title}
>
<Label>
Fuzzy search documentation
<AutocompleteInput placeholder="e.g. React" className="mt-2" />
</Label>
<AutocompleteContent sideOffset={4}>
<AutocompleteEmpty>
No results found for "<AutocompleteValue />"
</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.title} value={item}>
<AutocompleteValue>
{(value) => (
<div className="flex flex-col gap-1">
<div className="font-medium">
{highlightText(item.title,
value)}
</div>
<div className="text-sm text-muted-foreground">
{highlightText(item.description,
value)}
</div>
</div>
)}
</AutocompleteValue>
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
);
}
function highlightText(text: string,
query: string): React.ReactNode {
const trimmed = query.trim();
if (!trimmed) return text;
const escaped = trimmed.slice(0,
100).replace(/[.*+?^${}()|[\]\\]/g,
"\\$&");
const regex = new RegExp(`(${escaped})`,
"gi");
return text.split(regex).map((part,
idx) =>
regex.test(part) ? (
<mark key={idx} className="bg-yellow-200 dark:bg-yellow-900">
{part}
</mark>
) : (
part
)
);
}
function fuzzyFilter(item: unknown,
query: string): boolean {
if (!query) return true;
const fuzzyItem = item as FuzzyItem;
const results = matchSorter([fuzzyItem],
query,
{
keys: [
"title",
"description",
"category",
{ key: "title",
threshold: matchSorter.rankings.CONTAINS },
{ key: "description",
threshold: matchSorter.rankings.WORD_STARTS_WITH },
],
});
return results.length > 0;
}
interface FuzzyItem {
title: string;
description: string;
category: string;
}
const fuzzyItems: FuzzyItem[] = [
{
title: "React Hooks Guide",
description:
"Learn how to use React Hooks like useState,
useEffect,
and custom hooks",
category: "React",
},
// ... more items
];Virtualized List
Handle large datasets efficiently with @tanstack/react-virtual. This example demonstrates virtualization with 10,
000 items.
import * as React from "react";
import { Autocomplete } from "@tilt-legal/cubitt-components/primitives";
import {
AutocompleteClear,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
InputWrapper,
} from "@tilt-legal/cubitt-components/primitives";
import {
useVirtualizer } from "@tanstack/react-virtual";
export default function VirtualizedExample() {
const [open,
setOpen] = React.useState(false);
const [searchValue,
setSearchValue] = React.useState("");
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);
const { contains } = useAutocompleteFilter({ sensitivity: "base" });
const filteredItems = React.useMemo(() => {
if (!searchValue) return virtualItems;
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: 8,
paddingEnd: 8,
});
const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement) => {
scrollElementRef.current = element;
if (element) virtualizer.measure();
},
[virtualizer]
);
const totalSize = virtualizer.getTotalSize();
return (
<Autocomplete
items={filteredItems}
open={open}
onOpenChange={setOpen}
value={searchValue}
onValueChange={setSearchValue}
filter={null}
>
<Label>
Search 10,
000 items (virtualized)
<AutocompleteInput className="mt-2" />
</Label>
<AutocompleteContent sideOffset={4}>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{filteredItems.length > 0 && (
<div
role="presentation"
ref={handleScrollElementRef}
className="max-h-[300px] overflow-auto"
>
<div
role="presentation"
className="relative w-full"
style={{ height: `${totalSize}px` }}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = filteredItems[virtualItem.index];
if (!item) return null;
return (
<AutocompleteItem
key={virtualItem.key}
value={item}
className="absolute top-0 left-0 w-full"
style={{
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(4,
"0");
return `Item ${indexLabel}`;
});Size Variants
Three size variants: small, medium (default), and large.
import { Autocomplete } from "@tilt-legal/cubitt-components/primitives";
import { Label } from "@tilt-legal/cubitt-components/primitives";
// Small
<Autocomplete items={filteredItems}>
<InputWrapper>
<AutocompleteInput size="sm" placeholder="e.g. feature" />
{value && <AutocompleteClear />}
</InputWrapper>
<AutocompleteContent>
<AutocompleteList>
{(tag) => <AutocompleteItem key={tag.id} value={tag}>{tag.value}</AutocompleteItem>}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
// Medium (default)
<Autocomplete items={filteredItems}>
<InputWrapper>
<AutocompleteInput placeholder="e.g. feature" />
{value && <AutocompleteClear />}
</InputWrapper>
<AutocompleteContent>
<AutocompleteList>
{(tag) => <AutocompleteItem key={tag.id} value={tag}>{tag.value}</AutocompleteItem>}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
// Large
<Autocomplete items={filteredItems}>
<InputWrapper>
<AutocompleteInput size="lg" placeholder="e.g. feature" />
{value && <AutocompleteClear />}
</InputWrapper>
<AutocompleteContent>
<AutocompleteList>
{(tag) => <AutocompleteItem key={tag.id} value={tag}>{tag.value}</AutocompleteItem>}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>API Reference
Autocomplete
| Prop | Type | Description |
|---|---|---|
items | T[] | Array of items to display in the autocomplete list. Required |
value | string | number | string[] | The controlled value of the input. |
defaultValue | string | number | string[] | The default value when uncontrolled. |
onValueChange | (value: string | number | string[]) => void | Callback fired when the input value changes. |
open | boolean | The controlled open state of the popup. |
defaultOpen | boolean | The default open state when uncontrolled. Defaults to false. |
onOpenChange | (open: boolean, eventDetails) => void | Callback fired when the open state changes. |
autoHighlight | boolean | Automatically highlight the first matching item. Defaults to false. |
filter | (item: T, value: string) => boolean | null | Custom filter function. Set to null to disable filtering. |
itemToStringValue | (item: T) => string | Function to convert an item to a string value. |
name | string | Identifies the field when a form is submitted. |
disabled | boolean | Whether the autocomplete is disabled. Defaults to false. |
readOnly | boolean | Whether the autocomplete is read-only. Defaults to false. |
className | string | Additional CSS classes for the root element. |
AutocompleteInput
| Prop | Type | Description |
|---|---|---|
size | "sm" | "md" | "lg" | Size of the input. Defaults to "md". |
className | string | Additional CSS classes for the input. |
AutocompleteClear
Button to clear the input value.
| Prop | Type | Description |
|---|---|---|
children | ReactNode | Custom icon/content for the clear button. |
className | string | Additional CSS classes for the clear button. |
AutocompleteWrapper
Container for the input and clear button. Styled like InputWrapper and automatically serves as the anchor for popup positioning.
| Prop | Type | Description |
|---|---|---|
className | string | Additional CSS classes for the container. |
AutocompleteContent
Wrapper for the popup content, portal, and positioner.
| Prop | Type | Description |
|---|---|---|
align | "start" | "center" | "end" | Alignment of the popup. Defaults to "start". |
side | "top" | "bottom" | Side of the input to display popup. Defaults to "bottom". |
sideOffset | number | Offset from the input in pixels. Defaults to 4. |
alignOffset | number | Offset along the alignment axis. Defaults to 0. |
showBackdrop | boolean | Whether to show a backdrop overlay. Defaults to false. |
className | string | Additional CSS classes for the popup. |
AutocompleteList
Container for autocomplete items.
| Prop | Type | Description |
|---|---|---|
children | (item: T) => ReactElement | Render function for each item. |
className | string | Additional CSS classes for the list. |
AutocompleteItem
Individual selectable option.
| Prop | Type | Description |
|---|---|---|
value | T | The item value. |
className | string | Additional CSS classes for the item. |
AutocompleteGroup
Groups related items together.
| Prop | Type | Description |
|---|---|---|
items | T[] | Array of items in this group. |
className | string | Additional CSS classes for the group. |
AutocompleteGroupLabel
Label for a group of items.
| Prop | Type | Description |
|---|---|---|
className | string | Additional CSS classes for the group label. |
AutocompleteCollection
Nested collection for rendering grouped items.
| Prop | Type | Description |
|---|---|---|
children | (item: T) => ReactElement | Render function for each item. |
AutocompleteEmpty
Message displayed when no items match the search.
| Prop | Type | Description |
|---|---|---|
className | string | Additional CSS classes for the empty state. |
AutocompleteStatus
Status message displayed above the list (e.g., loading, results count).
| Prop | Type | Description |
|---|---|---|
className | string | Additional CSS classes for the status. |