Listbox
A shadcn-style listbox component built with Ark UI primitives.
Framework
React
Vue
Svelte
import { useMemo } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const ListboxDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox collection={collection} defaultValue={["react"]}>
<ListboxLabel>Framework</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxDemo;
Installation
npx shadcn@latest add @ark-cn/listboxInstall the dependency required by this primitive:
npm install @ark-ui/react lucide-reactCopy the component source into your app:
TSXcomponents/ui/listbox.tsx
"use client";
import {
type CollectionItem,
Listbox as ListboxPrimitive,
} from "@ark-ui/react/listbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
export type ListboxProps<T extends CollectionItem = CollectionItem> =
ListboxPrimitive.RootProps<T>;
export const Listbox = <T extends CollectionItem = CollectionItem>(
props: ListboxPrimitive.RootProps<T>,
) => {
const { className, ...rest } = props;
return (
<ListboxPrimitive.Root
data-slot="listbox"
className={cn("flex w-full flex-col gap-2", className)}
{...rest}
/>
);
};
export type ListboxLabelProps = ListboxPrimitive.LabelProps;
export const ListboxLabel = ({ className, ...props }: ListboxLabelProps) => (
<ListboxPrimitive.Label
data-slot="listbox-label"
className={cn(
"text-sm font-medium text-foreground leading-none select-none data-disabled:opacity-64",
className,
)}
{...props}
/>
);
export type ListboxContentProps = ListboxPrimitive.ContentProps;
export const ListboxContent = ({
className,
...props
}: ListboxContentProps) => (
<ListboxPrimitive.Content
data-slot="listbox-content"
className={cn(
"max-h-60 min-h-10 overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-sm outline-none ring-1 ring-border/20",
"data-[orientation=vertical]:flex data-[orientation=vertical]:flex-col data-[orientation=vertical]:gap-0.5",
"data-[orientation=horizontal]:flex data-[orientation=horizontal]:flex-row data-[orientation=horizontal]:flex-wrap data-[orientation=horizontal]:gap-1.5 data-[orientation=horizontal]:content-start",
"data-[layout=grid]:grid data-[layout=grid]:gap-1 data-[layout=grid]:auto-rows-fr data-[layout=grid]:grid-cols-[repeat(var(--column-count),minmax(0,1fr))]",
className,
)}
{...props}
/>
);
export type ListboxEmptyProps = ListboxPrimitive.EmptyProps;
export const ListboxEmpty = ({ className, ...props }: ListboxEmptyProps) => (
<ListboxPrimitive.Empty
data-slot="listbox-empty"
className={cn(
"rounded-md px-2 py-6 text-center text-muted-foreground text-sm",
className,
)}
{...props}
/>
);
export type ListboxInputProps = ListboxPrimitive.InputProps;
export const ListboxInput = ({ className, ...props }: ListboxInputProps) => (
<ListboxPrimitive.Input
data-slot="listbox-input"
className={cn(
"h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-foreground text-sm shadow-xs outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-64",
className,
)}
{...props}
/>
);
export type ListboxItemProps = ListboxPrimitive.ItemProps;
export const ListboxItem = ({ className, ...props }: ListboxItemProps) => (
<ListboxPrimitive.Item
data-slot="listbox-item"
className={cn(
"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors",
"data-disabled:pointer-events-none data-disabled:opacity-50",
"data-highlighted:bg-accent data-highlighted:text-accent-foreground",
"data-selected:bg-accent/80",
"data-[orientation=horizontal]:shrink-0",
"data-[layout=grid]:min-h-9 data-[layout=grid]:min-w-0",
className,
)}
{...props}
/>
);
export type ListboxItemTextProps = ListboxPrimitive.ItemTextProps;
export const ListboxItemText = ({
className,
...props
}: ListboxItemTextProps) => (
<ListboxPrimitive.ItemText
data-slot="listbox-item-text"
className={cn("flex-1 truncate", className)}
{...props}
/>
);
export type ListboxItemIndicatorProps = ListboxPrimitive.ItemIndicatorProps;
export const ListboxItemIndicator = ({
className,
children,
...props
}: ListboxItemIndicatorProps) => (
<ListboxPrimitive.ItemIndicator
data-slot="listbox-item-indicator"
className={cn("text-primary shrink-0", className)}
{...props}
>
{children ?? <CheckIcon className="size-4" />}
</ListboxPrimitive.ItemIndicator>
);
export type ListboxItemGroupProps = ListboxPrimitive.ItemGroupProps;
export const ListboxItemGroup = ({
className,
...props
}: ListboxItemGroupProps) => (
<ListboxPrimitive.ItemGroup
data-slot="listbox-item-group"
className={cn(
"flex flex-col gap-0.5",
"data-[orientation=horizontal]:flex-row data-[orientation=horizontal]:flex-wrap data-[orientation=horizontal]:gap-2",
className,
)}
{...props}
/>
);
export type ListboxItemGroupLabelProps = ListboxPrimitive.ItemGroupLabelProps;
export const ListboxItemGroupLabel = ({
className,
...props
}: ListboxItemGroupLabelProps) => (
<ListboxPrimitive.ItemGroupLabel
data-slot="listbox-item-group-label"
className={cn(
"px-2 py-1 font-medium text-muted-foreground text-xs",
className,
)}
{...props}
/>
);
export type ListboxValueTextProps = ListboxPrimitive.ValueTextProps;
export const ListboxValueText = ({
className,
...props
}: ListboxValueTextProps) => (
<ListboxPrimitive.ValueText
data-slot="listbox-value-text"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
export const ListboxContext = ListboxPrimitive.Context;
export type ListboxRootProviderProps<
T extends CollectionItem = CollectionItem,
> = ListboxPrimitive.RootProviderProps<T>;
export const ListboxRootProvider = <T extends CollectionItem = CollectionItem>(
props: ListboxPrimitive.RootProviderProps<T>,
) => <ListboxPrimitive.RootProvider {...props} />;
/** Grid collections and `useListCollection` (filtering) live in `@ark-ui/react/collection`. */
export {
createGridCollection,
type GridCollection,
type GridCollectionOptions,
type UseListCollectionProps,
type UseListCollectionReturn,
useListCollection,
} from "@ark-ui/react/collection";
export type {
CollectionItem,
ListboxHighlightChangeDetails,
ListboxScrollToIndexDetails,
ListboxSelectionDetails,
ListboxSelectionMode,
ListboxValueChangeDetails,
ListCollection,
UseListboxItemContext,
UseListboxProps,
UseListboxReturn,
} from "@ark-ui/react/listbox";
export {
createListCollection,
useListbox,
useListboxContext,
useListboxItemContext,
} from "@ark-ui/react/listbox";
Update import aliases to match your project setup.
Usage
import * as Listbox from "@/components/ui/listbox"Read exported parts in src/components/ui/listbox.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
Framework
React
Vue
Svelte
import { useMemo } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const ListboxBasicDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox collection={collection} defaultValue={["react"]}>
<ListboxLabel>Framework</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxBasicDemo;
Controlled
Controlled
React
Vue
Svelte
value: vue
import { useMemo, useState } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const ListboxControlledDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
],
}),
[],
);
const [value, setValue] = useState<string[]>(["vue"]);
return (
<div className="w-full max-w-sm">
<Listbox
collection={collection}
onValueChange={(d) => setValue(d.value)}
value={value}
>
<ListboxLabel>Controlled</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
<p className="mt-2 text-muted-foreground text-xs">
value: {value.join(", ") || "—"}
</p>
</div>
);
};
export default ListboxControlledDemo;
Root provider
useListbox + RootProvider
Option A
Option B
import { useMemo } from "react";
import {
createListCollection,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
ListboxRootProvider,
useListbox,
} from "@/components/ui/listbox";
const ListboxRootProviderDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "Option A", value: "a" },
{ label: "Option B", value: "b" },
],
}),
[],
);
const service = useListbox({ collection, defaultValue: ["a"] });
return (
<ListboxRootProvider
className="flex w-full max-w-sm flex-col gap-2"
value={service}
>
<ListboxLabel>useListbox + RootProvider</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</ListboxRootProvider>
);
};
export default ListboxRootProviderDemo;
Disabled
Disabled item
Available
Unavailable
Available
import { useMemo } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const ListboxDisabledDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "Available", value: "a" },
{ label: "Unavailable", value: "b" },
{ label: "Available", value: "c" },
],
isItemDisabled: (item) => item.value === "b",
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox collection={collection} defaultValue={["a"]}>
<ListboxLabel>Disabled item</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxDisabledDemo;
Multiple
Multiple selection
Apple
Banana
Orange
import { useMemo } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const ListboxMultipleDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
],
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox
collection={collection}
defaultValue={["apple", "orange"]}
selectionMode="multiple"
>
<ListboxLabel>Multiple selection</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxMultipleDemo;
Grouped
Grouped
UI
React
Vue
Svelte
Runtime
Node.js
import { useMemo } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemGroup,
ListboxItemGroupLabel,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const LISTBOX_ECOSYSTEM = [
{ ecosystem: "UI", label: "React", value: "react" },
{ ecosystem: "UI", label: "Vue", value: "vue" },
{ ecosystem: "UI", label: "Svelte", value: "svelte" },
{ ecosystem: "Runtime", label: "Node.js", value: "node" },
] as const;
const ListboxGroupedDemo = () => {
const collection = useMemo(
() =>
createListCollection({
groupBy: (item) => item.ecosystem,
items: [...LISTBOX_ECOSYSTEM],
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox collection={collection} defaultValue={["react"]}>
<ListboxLabel>Grouped</ListboxLabel>
<ListboxContent>
{collection.group().map(([group, items]) => (
<ListboxItemGroup key={group}>
<ListboxItemGroupLabel>{group}</ListboxItemGroupLabel>
{items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxItemGroup>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxGroupedDemo;
Extended selection
Extended (Ctrl + click)
One
Two
Three
Four
import { useMemo } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const ListboxExtendedDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "One", value: "1" },
{ label: "Two", value: "2" },
{ label: "Three", value: "3" },
{ label: "Four", value: "4" },
],
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox collection={collection} selectionMode="extended">
<ListboxLabel>Extended (Ctrl + click)</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxExtendedDemo;
Horizontal
Horizontal orientation
Sm
Md
Lg
Xl
import { useMemo } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const ListboxHorizontalDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "Sm", value: "sm" },
{ label: "Md", value: "md" },
{ label: "Lg", value: "lg" },
{ label: "Xl", value: "xl" },
],
}),
[],
);
return (
<div className="w-full max-w-md">
<Listbox
collection={collection}
defaultValue={["md"]}
orientation="horizontal"
>
<ListboxLabel>Horizontal orientation</ListboxLabel>
<ListboxContent className="max-h-none min-h-0">
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxHorizontalDemo;
Grid
Grid (3 columns)
Cell 1
Cell 2
Cell 3
Cell 4
Cell 5
Cell 6
Cell 7
Cell 8
Cell 9
import { useMemo } from "react";
import {
createGridCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const LISTBOX_GRID_CELLS = Array.from({ length: 9 }, (_, i) => ({
label: `Cell ${i + 1}`,
value: `c${i + 1}`,
}));
const ListboxGridDemo = () => {
const collection = useMemo(
() =>
createGridCollection({
columnCount: 3,
items: LISTBOX_GRID_CELLS,
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox collection={collection} defaultValue={["c1"]}>
<ListboxLabel>Grid (3 columns)</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxGridDemo;
Filtering
Filter
Apple
Banana
Orange
Grape
Strawberry
Mango
Pineapple
Kiwi
Peach
Pear
import { useEffect, useState } from "react";
import {
Listbox,
ListboxContent,
ListboxEmpty,
ListboxInput,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
useListCollection,
} from "@/components/ui/listbox";
const FRUITS = [
{ 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" },
] as const;
const ListboxFilteringDemo = () => {
const [query, setQuery] = useState("");
const { collection, filter } = useListCollection({
filter: (itemText, filterText) =>
itemText.toLowerCase().includes(filterText.toLowerCase()),
initialItems: [...FRUITS],
});
useEffect(() => {
filter(query);
}, [query, filter]);
return (
<div className="w-full max-w-sm">
<Listbox collection={collection}>
<ListboxLabel>Filter</ListboxLabel>
<ListboxInput
onChange={(e) => setQuery(e.target.value)}
placeholder="Search fruits..."
value={query}
/>
<ListboxContent>
<ListboxEmpty>No matches.</ListboxEmpty>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxFilteringDemo;
Select all
Fruits
Apple
Banana
Orange
Grape
Strawberry
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxContext,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
} from "@/components/ui/listbox";
const FRUITS = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
] as const;
const ListboxSelectAllDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [...FRUITS],
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox
collection={collection}
defaultValue={[]}
selectionMode="multiple"
>
<div className="flex flex-wrap gap-2">
<ListboxContext>
{(api) => (
<>
<Button
onClick={() => api.selectAll()}
size="sm"
type="button"
variant="outline"
>
Select all
</Button>
<Button
onClick={() => api.clearValue()}
size="sm"
type="button"
variant="outline"
>
Clear
</Button>
</>
)}
</ListboxContext>
</div>
<ListboxLabel>Fruits</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
</Listbox>
</div>
);
};
export default ListboxSelectAllDemo;
Value text
Value text
React
Vue
Selected: None yet
import { useMemo } from "react";
import {
createListCollection,
Listbox,
ListboxContent,
ListboxItem,
ListboxItemIndicator,
ListboxItemText,
ListboxLabel,
ListboxValueText,
} from "@/components/ui/listbox";
const ListboxValueTextDemo = () => {
const collection = useMemo(
() =>
createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
],
}),
[],
);
return (
<div className="w-full max-w-sm">
<Listbox collection={collection}>
<ListboxLabel>Value text</ListboxLabel>
<ListboxContent>
{collection.items.map((item) => (
<ListboxItem key={item.value} item={item}>
<ListboxItemText>{item.label}</ListboxItemText>
<ListboxItemIndicator />
</ListboxItem>
))}
</ListboxContent>
<p className="text-muted-foreground text-sm">
Selected:{" "}
<ListboxValueText
className="font-medium text-foreground"
placeholder="None yet"
/>
</p>
</Listbox>
</div>
);
};
export default ListboxValueTextDemo;
API reference
This component mirrors the upstream Ark UI primitive.
See the ARK UI documentation for the full API.