Combobox
An autocomplete input component that allows users to filter and select options from a list by typing.
import { type ComboboxInputValueChangeDetails, useFilter, useListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import {
Combobox,
ComboboxLabel,
ComboboxInputTrigger,
ComboboxContent,
ComboboxItem,
} from "~/components/combobox";
export default function ComboboxBasicDemo() {
const filterFn = useFilter({ sensitivity: "base" });
const { collection, filter } = useListCollection({
initialItems: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
filter: filterFn().contains,
});
const handleInputChange = (details: ComboboxInputValueChangeDetails) => {
filter(details.inputValue);
};
return (
<div>
<Combobox collection={collection()} onInputValueChange={handleInputChange}>
<ComboboxLabel>Framework</ComboboxLabel>
<ComboboxInputTrigger placeholder="Search frameworks..." />
<ComboboxContent>
<Index each={collection().items}>
{(item) => <ComboboxItem item={item()}>{item().label}</ComboboxItem>}
</Index>
</ComboboxContent>
</Combobox>
</div>
);
}Installation
CLI
Run the following command to add the component to your project:
npx @ark-preset/cli@latest add comboboxManual
Create the recipe file at src/components/recipes/combobox.ts:
import { tv, type VariantProps } from "tailwind-variants";
export const comboboxVariants = tv({
slots: {
root: "grid gap-1.5 w-full group/combobox",
control:
"flex h-8 w-full items-center justify-between rounded-md border border-input bg-background px-2.5 py-1 text-sm ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 group-data-[error]/combobox:border-destructive group-data-[error]/combobox:focus-within:ring-destructive",
input:
"flex flex-1 items-center justify-start bg-transparent text-base md:text-sm placeholder:text-muted-foreground outline-none disabled:cursor-not-allowed disabled:opacity-50",
trigger:
"flex size-4 items-center justify-center text-muted-foreground [&[data-state=open]>svg]:rotate-180",
clearTrigger:
"flex size-4 items-center justify-center text-muted-foreground hover:text-foreground",
positioner: "z-50",
content:
"z-50 min-w-[8rem] rounded-md border border-border bg-background p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
list: "space-y-1",
item: "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
itemText: "flex-1",
itemIndicator: "absolute right-2 flex size-4 items-center justify-center",
empty: "py-2 text-center text-sm text-muted-foreground",
},
});
export type ComboboxVariants = VariantProps<typeof comboboxVariants>;Create the component directory and files.
src/components/combobox/combobox.base.tsx:
import { Combobox as ArkCombobox } from "@ark-ui/solid/combobox";
import { splitProps, type Component } from "solid-js";
import { comboboxVariants, labelVariants } from "../recipes/combobox";
const styles = comboboxVariants();
type ComboboxRootProps = ArkCombobox.RootProps<{ label: string; value: string }> & {
error?: boolean;
};
const Root: Component<ComboboxRootProps> = (props) => {
const [local, others] = splitProps(props, ["class", "error"]);
return (
<ArkCombobox.Root
class={styles.root({ class: local.class })}
data-error={local.error ? "" : undefined}
{...others}
/>
);
};
type ComboboxRootProviderProps = ArkCombobox.RootProviderProps<{ label: string; value: string }>;
const RootProvider: Component<ComboboxRootProviderProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.RootProvider class={styles.root({ class: local.class })} {...others} />;
};
const Label: Component<ArkCombobox.LabelProps & { error?: boolean }> = (props) => {
const [local, others] = splitProps(props, ["class", "error"]);
return (
<ArkCombobox.Label
class={labelVariants({ class: local.class, error: local.error })}
{...others}
/>
);
};
const Input: Component<ArkCombobox.InputProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.Input class={styles.input({ class: local.class })} {...others} />;
};
const Trigger: Component<ArkCombobox.TriggerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.Trigger class={styles.trigger({ class: local.class })} {...others} />;
};
const Positioner: Component<ArkCombobox.PositionerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.Positioner class={styles.positioner({ class: local.class })} {...others} />;
};
const List: Component<ArkCombobox.ListProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.List class={styles.list({ class: local.class })} {...others} />;
};
const Item: Component<ArkCombobox.ItemProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.Item class={styles.item({ class: local.class })} {...others} />;
};
const ItemText: Component<ArkCombobox.ItemTextProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.ItemText class={styles.itemText({ class: local.class })} {...others} />;
};
const ItemIndicator: Component<ArkCombobox.ItemIndicatorProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<ArkCombobox.ItemIndicator class={styles.itemIndicator({ class: local.class })} {...others} />
);
};
const Empty: Component<ArkCombobox.EmptyProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.Empty class={styles.empty({ class: local.class })} {...others} />;
};
const Control: Component<ArkCombobox.ControlProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.Control class={styles.control({ class: local.class })} {...others} />;
};
const ClearTrigger: Component<ArkCombobox.ClearTriggerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<ArkCombobox.ClearTrigger class={styles.clearTrigger({ class: local.class })} {...others} />
);
};
const Content: Component<ArkCombobox.ContentProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return <ArkCombobox.Content class={styles.content({ class: local.class })} {...others} />;
};
const ItemGroup = ArkCombobox.ItemGroup;
const ItemGroupLabel = ArkCombobox.ItemGroupLabel;
export const Combobox = {
Root,
RootProvider,
Label,
Input,
Trigger,
Positioner,
List,
Item,
ItemText,
ItemIndicator,
Empty,
Control,
ClearTrigger,
Content,
ItemGroup,
ItemGroupLabel,
};src/components/combobox/index.tsx:
import { Combobox as ComboboxBase } from "./combobox.base";
import type { Combobox as ArkCombobox } from "@ark-ui/solid/combobox";
import { Portal } from "solid-js/web";
import { splitProps, type Component, type JSX } from "solid-js";
import { ScrollArea } from "../scroll-area";
const Combobox = ComboboxBase.Root;
const ComboboxLabel = ComboboxBase.Label;
type InputTriggerProps = ArkCombobox.InputProps;
const InputTrigger: Component<InputTriggerProps> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<ComboboxBase.Control class={local.class}>
<ComboboxBase.Input {...others} />
<ComboboxBase.ClearTrigger>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</ComboboxBase.ClearTrigger>
<ComboboxBase.Trigger>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="m6 9 6 6 6-6" />
</svg>
</ComboboxBase.Trigger>
</ComboboxBase.Control>
);
};
type ContentProps = ArkCombobox.ContentProps & {
class?: string;
children?: JSX.Element;
};
const Content: Component<ContentProps> = (props) => {
const [local, others] = splitProps(props, ["class", "children"]);
return (
<Portal>
<ComboboxBase.Positioner>
<ComboboxBase.Content class={local.class} {...others}>
<ComboboxBase.List>
<ScrollArea class="max-h-60" orientation="vertical">
{local.children}
</ScrollArea>
</ComboboxBase.List>
</ComboboxBase.Content>
</ComboboxBase.Positioner>
</Portal>
);
};
type ItemProps = ArkCombobox.ItemProps & { class?: string; children?: JSX.Element };
const Item: Component<ItemProps> = (props) => {
const [local, others] = splitProps(props, ["class", "children"]);
return (
<ComboboxBase.Item class={local.class} {...others}>
<ComboboxBase.ItemText>{local.children}</ComboboxBase.ItemText>
<ComboboxBase.ItemIndicator>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M20 6 9 17l-5-5" />
</svg>
</ComboboxBase.ItemIndicator>
</ComboboxBase.Item>
);
};
const ComboboxRootProvider = ComboboxBase.RootProvider;
const ComboboxInputTrigger = InputTrigger;
const ComboboxContent = Content;
const ComboboxItem = Item;
export {
Combobox,
ComboboxLabel,
ComboboxInputTrigger,
ComboboxContent,
ComboboxItem,
ComboboxRootProvider,
ComboboxBase,
};
export { comboboxVariants, type ComboboxVariants } from "../recipes/combobox";Note: Make sure your project has the Tailwind CSS theme variables set up (
--background,--foreground,--ring,--border, etc.) or override the utility classes to match your design system.
Dependencies: This component imports shared recipes or sub-components from other packages. Make sure the following are also installed: scroll-area.
Usage
Basic Usage
Client-side filtering.
import { type ComboboxInputValueChangeDetails, useFilter, useListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import {
Combobox,
ComboboxLabel,
ComboboxInputTrigger,
ComboboxContent,
ComboboxItem,
} from "~/components/combobox";
export default function ComboboxBasicDemo() {
const filterFn = useFilter({ sensitivity: "base" });
const { collection, filter } = useListCollection({
initialItems: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
filter: filterFn().contains,
});
const handleInputChange = (details: ComboboxInputValueChangeDetails) => {
filter(details.inputValue);
};
return (
<div>
<Combobox collection={collection()} onInputValueChange={handleInputChange}>
<ComboboxLabel>Framework</ComboboxLabel>
<ComboboxInputTrigger placeholder="Search frameworks..." />
<ComboboxContent>
<Index each={collection().items}>
{(item) => <ComboboxItem item={item()}>{item().label}</ComboboxItem>}
</Index>
</ComboboxContent>
</Combobox>
</div>
);
}Error State
Use the error prop on Combobox to show an error state. This will automatically highlight the input trigger with a destructive red border:
import { type ComboboxInputValueChangeDetails, useFilter, useListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import {
Combobox,
ComboboxLabel,
ComboboxInputTrigger,
ComboboxContent,
ComboboxItem,
} from "~/components/combobox";
export default function ComboboxErrorDemo() {
const filterFn = useFilter({ sensitivity: "base" });
const { collection, filter } = useListCollection({
initialItems: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
filter: filterFn().contains,
});
const handleInputChange = (details: ComboboxInputValueChangeDetails) => {
filter(details.inputValue);
};
return (
<div>
<Combobox collection={collection()} onInputValueChange={handleInputChange} error>
<ComboboxLabel>Framework</ComboboxLabel>
<ComboboxInputTrigger placeholder="Search frameworks..." />
<ComboboxContent>
<Index each={collection().items}>
{(item) => <ComboboxItem item={item()}>{item().label}</ComboboxItem>}
</Index>
</ComboboxContent>
</Combobox>
</div>
);
}Composite Exports
The ComboboxInputTrigger, ComboboxContent, and ComboboxItem components are composite exports from the barrel entry point that include inline SVG icons and Portal. Import them alongside Combobox:
import { Combobox, ComboboxLabel, ComboboxInputTrigger, ComboboxContent, ComboboxItem } from "~/components/combobox";Advanced Usage
When you need more control, import raw primitive parts directly from the base file:
import { Combobox } from "~/components/combobox/combobox.base";Or import ComboboxBase (the raw parts namespace) from the barrel entry point:
import { ComboboxBase } from "~/components/combobox";Root Provider
Use ComboboxRootProvider when you need to access the combobox state outside of the component tree. This pattern uses the useCombobox hook from Ark UI to create a shared context that both the combobox and external elements can reference.
import { useFilter } from "@ark-ui/solid";
import { useCombobox, useListCollection } from "@ark-ui/solid/combobox";
import { Index } from "solid-js";
import {
ComboboxRootProvider,
ComboboxLabel,
ComboboxInputTrigger,
ComboboxContent,
ComboboxItem,
} from "~/components/combobox";
export default function ComboboxRootProviderDemo() {
const filterFn = useFilter({ sensitivity: "base" });
const { collection, filter } = useListCollection({
initialItems: [
{ label: "React", value: "react" },
{ label: "Solid.js", value: "solid" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
filter: filterFn().contains,
});
const combobox = useCombobox({
get collection() {
return collection();
},
defaultValue: ["solid"],
onInputValueChange(details) {
filter(details.inputValue);
},
});
return (
<div class="space-y-4">
<output class="block text-sm text-muted-foreground">
Value: {JSON.stringify(combobox().value)}
</output>
<ComboboxRootProvider value={combobox}>
<ComboboxLabel>Framework</ComboboxLabel>
<ComboboxInputTrigger placeholder="Search frameworks..." />
<ComboboxContent>
<Index each={collection().items}>
{(item) => <ComboboxItem item={item()}>{item().label}</ComboboxItem>}
</Index>
</ComboboxContent>
</ComboboxRootProvider>
</div>
);
}The key difference:
Combobox— manages its own state internally. Use for simple, self-contained comboboxes.ComboboxRootProvider— accepts a pre-created combobox context viauseCombobox. Use when you need to read or control the combobox state from outside the component tree.
API Reference
See the Ark UI Combobox documentation.