Autocomplete
A searchable text input that suggests and filters matching options as you type.
Basic
Enter a registry URL with optional tags.
Show codeHide code
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInputGroup,
AutocompleteItem,
AutocompleteList,
} from "@makeplane/propel/components/autocomplete";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@makeplane/propel/components/field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ChevronsUpDown, X } from "lucide-react";
const IMAGES = ["nginx:1.29-alpine", "node:22-slim", "postgres:18", "redis:8.2.2-alpine"];
export default function BasicDemo() {
return (
<Field name="containerImage">
<Autocomplete items={IMAGES} mode="both" required>
<FieldLabel magnitude="md" inset={false}>
Container image
</FieldLabel>
<AutocompleteInputGroup
magnitude="md"
placeholder="e.g. docker.io/library/node:latest"
clear={
<IconButton
prominence="ghost"
tone="neutral"
magnitude="md"
aria-label="Clear container image"
icon={<Icon icon={X} />}
/>
}
trigger={
<IconButton
prominence="ghost"
tone="neutral"
magnitude="md"
aria-label="Open container image"
icon={<Icon icon={ChevronsUpDown} />}
/>
}
/>
<FieldDescription magnitude="md">Enter a registry URL with optional tags.</FieldDescription>
<AutocompleteContent>
<AutocompleteEmpty>No matches</AutocompleteEmpty>
<AutocompleteList>
{(image: string) => (
<AutocompleteItem key={image} value={image} magnitude="md">
{image}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
<FieldError magnitude="md" />
</Autocomplete>
</Field>
);
}Search
Show codeHide code
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInputGroup,
AutocompleteItem,
AutocompleteList,
} from "@makeplane/propel/components/autocomplete";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { Search, X } from "lucide-react";
const IMAGES = ["nginx:1.29-alpine", "node:22-slim", "postgres:18", "redis:8.2.2-alpine"];
export default function SearchDemo() {
return (
<Autocomplete items={IMAGES} mode="both">
<AutocompleteInputGroup
magnitude="md"
icon={<Icon icon={Search} tint="placeholder" />}
placeholder="Search images"
aria-label="Search images"
clear={
<IconButton
prominence="ghost"
tone="neutral"
magnitude="md"
aria-label="Clear search"
icon={<Icon icon={X} />}
/>
}
/>
<AutocompleteContent>
<AutocompleteEmpty>No matches</AutocompleteEmpty>
<AutocompleteList>
{(image: string) => (
<AutocompleteItem key={image} value={image} magnitude="md">
{image}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
);
}Grouped
Grouped suggestions render each group under a label; filtering drops empty groups and their headings automatically.
Show codeHide code
import {
Autocomplete,
AutocompleteCollection,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteGroupLabel,
AutocompleteInputGroup,
AutocompleteItem,
AutocompleteList,
} from "@makeplane/propel/components/autocomplete";
const GROUPED_LABELS = [
{ label: "Type", items: ["feature", "fix", "bug", "docs"] },
{ label: "Component", items: ["component: editor", "component: sidebar", "component: issues"] },
];
export default function GroupedDemo() {
return (
<Autocomplete items={GROUPED_LABELS}>
<AutocompleteInputGroup magnitude="md" placeholder="e.g. feature" aria-label="Add label" />
<AutocompleteContent>
<AutocompleteEmpty>No matches</AutocompleteEmpty>
<AutocompleteList>
{(group: (typeof GROUPED_LABELS)[number]) => (
<AutocompleteGroup key={group.label} items={group.items}>
<AutocompleteGroupLabel>{group.label}</AutocompleteGroupLabel>
<AutocompleteCollection>
{(label: string) => (
<AutocompleteItem key={label} value={label} magnitude="md">
{label}
</AutocompleteItem>
)}
</AutocompleteCollection>
</AutocompleteGroup>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
);
}Async
Hand filtering to the consumer with filter={null}, drive the lookup from value/onValueChange, and announce progress with AutocompleteStatus.
Show codeHide code
import {
Autocomplete,
AutocompleteContent,
AutocompleteInputGroup,
AutocompleteItem,
AutocompleteList,
AutocompleteStatus,
useFilter,
} from "@makeplane/propel/components/autocomplete";
import { Icon } from "@makeplane/propel/components/icon";
import { Search } from "lucide-react";
import * as React from "react";
const PROJECTS = ["Design system", "Marketing site", "Mobile app", "Platform API"];
export default function AsyncSearchDemo() {
const [value, setValue] = React.useState("");
const [results, setResults] = React.useState<readonly string[]>([]);
const [searching, setSearching] = React.useState(false);
const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const { contains } = useFilter();
React.useEffect(() => () => clearTimeout(timeoutRef.current), []);
return (
<Autocomplete
items={results}
value={value}
filter={null}
onValueChange={(nextValue) => {
setValue(nextValue);
clearTimeout(timeoutRef.current);
if (nextValue === "") {
setSearching(false);
setResults([]);
return;
}
setSearching(true);
timeoutRef.current = setTimeout(() => {
setResults(PROJECTS.filter((project) => contains(project, nextValue)));
setSearching(false);
}, 300);
}}
>
<AutocompleteInputGroup
magnitude="md"
icon={<Icon icon={Search} tint="placeholder" />}
placeholder="Search projects"
aria-label="Search projects"
/>
<AutocompleteContent>
<AutocompleteStatus>
{searching
? "Searching…"
: value !== "" && `${results.length} result${results.length === 1 ? "" : "s"}`}
</AutocompleteStatus>
<AutocompleteList>
{(project: string) => (
<AutocompleteItem key={project} value={project} magnitude="md">
{project}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
);
}Fuzzy matching
A custom filter replaces the built-in contains match, so skipped-letter queries (e.g. ngx) still find their item.
Show codeHide code
import {
Autocomplete,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteInputGroup,
AutocompleteItem,
AutocompleteList,
} from "@makeplane/propel/components/autocomplete";
import { Icon } from "@makeplane/propel/components/icon";
import { Search } from "lucide-react";
const IMAGES = ["nginx:1.29-alpine", "node:22-slim", "postgres:18", "redis:8.2.2-alpine"];
// A subsequence matcher: every query character must appear in the item, in order — so partial or
// skipped-letter queries ("ngx", "pstgrs") still find their item.
function fuzzyMatch(item: string, query: string): boolean {
const needle = query.trim().toLowerCase();
if (needle === "") {
return true;
}
let matched = 0;
for (const char of item.toLowerCase()) {
if (char === needle[matched]) {
matched += 1;
}
if (matched === needle.length) {
return true;
}
}
return false;
}
export default function FuzzyMatchingDemo() {
return (
<Autocomplete items={IMAGES} filter={fuzzyMatch}>
<AutocompleteInputGroup
magnitude="md"
icon={<Icon icon={Search} tint="placeholder" />}
placeholder="Search images"
aria-label="Search images"
/>
<AutocompleteContent>
<AutocompleteEmpty>No matches</AutocompleteEmpty>
<AutocompleteList>
{(image: string) => (
<AutocompleteItem key={image} value={image} magnitude="md">
{image}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
);
}Installation
import {
Autocomplete,
AutocompleteInputGroup,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteList,
AutocompleteItem,
} from "@makeplane/propel/components/autocomplete";
Props
Autocomplete
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | The autocomplete's anatomy — an `AutocompleteInput` and the `AutocompletePopup` of items. |
AutocompleteInputGroup
| Prop | Type | Required | Description |
|---|---|---|---|
| magnitude | "sm" | "md" | "lg" | "xl" | Yes | Visual size of the input row: height, padding, and icon/text sizing. Required. |
| icon | ReactNode | No | Decorative leading element at the inline-start, e.g. `<Icon icon={Search} tint="placeholder" />`. |
| clear | ReactElement<unknown, string | JSXElementConstructor<any>> | No | The clear control (e.g. an `IconButton`), grafted onto Base UI's `Clear` behavior. It carries its own — localizable — `aria-label`; the group bakes no label or glyph. |
| trigger | ReactElement<unknown, string | JSXElementConstructor<any>> | No | The popup-trigger control (e.g. an `IconButton`), grafted onto Base UI's `Trigger` behavior. It carries its own — localizable — `aria-label`; the group bakes no label or glyph. |
AutocompleteItem
| Prop | Type | Required | Description |
|---|---|---|---|
| magnitude | "sm" | "md" | "lg" | Yes | Visual size of the row: height and text size. Required. |