Select
A shadcn-style select component built with Ark UI primitives.
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectBasic = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Select framework"
defaultValue={["next"]}
items={frameworks}
>
<SelectTriggerField>
<SelectValue placeholder="Select framework" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectBasic;
Installation
npx shadcn@latest add @ark-cn/selectInstall the dependency required by this primitive:
npm install @ark-ui/react lucide-react class-variance-authorityCopy the component source into your app:
TSXcomponents/ui/select.tsx
"use client";
import { Portal } from "@ark-ui/react/portal";
import {
type CollectionItem,
createListCollection,
type ListCollection,
Select as SelectPrimitive,
type SelectRootProps,
SelectRootProvider,
type UseSelectContext,
type UseSelectReturn,
useListCollection,
useSelect,
useSelectContext,
} from "@ark-ui/react/select";
import type { VariantProps } from "class-variance-authority";
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
import {
type ComponentProps,
Fragment,
type ReactNode,
useEffect,
useMemo,
} from "react";
import { Button, buttonVariants } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { cn } from "@/lib/utils";
export type {
CollectionItem,
ListCollection,
UseSelectContext,
UseSelectReturn,
};
export {
createListCollection,
SelectRootProvider,
useListCollection,
useSelect,
useSelectContext,
};
export type SelectProps<T extends CollectionItem = CollectionItem> = Omit<
SelectRootProps<T>,
"collection" | "children"
> & {
children: ReactNode | ((api: UseSelectContext<T>) => ReactNode);
} & (
| {
items: readonly T[];
groupBy?: (item: T, index: number) => string;
groupSort?:
| string[]
| "asc"
| "desc"
| ((a: string, b: string) => number);
itemToString?: (item: T) => string;
itemToValue?: (item: T) => string;
isItemDisabled?: (item: T) => boolean;
}
| { collection: ListCollection<T> }
);
/** Strip `useListCollection` helpers from the root spread so they are not passed to a DOM node. */
type SelectListCollectionKeys<T extends CollectionItem> = {
items?: readonly T[];
groupBy?: (item: T, index: number) => string;
groupSort?: string[] | "asc" | "desc" | ((a: string, b: string) => number);
itemToString?: (item: T) => string;
itemToValue?: (item: T) => string;
isItemDisabled?: (item: T) => boolean;
collection?: ListCollection<T>;
};
export const Select = <T extends CollectionItem = CollectionItem>(
props: SelectProps<T>,
) => {
const {
positioning,
children,
items: itemsFromProps,
groupBy,
groupSort,
itemToString,
itemToValue,
isItemDisabled,
collection: collectionProp,
...rootProps
} = props as SelectProps<T> & SelectListCollectionKeys<T>;
const items = itemsFromProps !== undefined ? itemsFromProps : [];
const { collection: defaultCollection, set } = useListCollection<T>({
initialItems: items,
groupBy,
groupSort,
itemToString,
itemToValue,
isItemDisabled,
});
const itemsKey = useMemo(
() =>
items
.map((item: T) =>
String(
itemToValue?.(item) ?? (item as { value?: string }).value ?? "",
),
)
.join("\uffff"),
[items, itemToValue],
);
useEffect(() => {
// Sync `items` into the internal collection only when the *contents* change.
// Avoids infinite render loops when callers pass `items={[...arr]}`.
set([...items]);
}, [itemsKey]);
return (
<SelectPrimitive.Root
collection={collectionProp ?? defaultCollection}
data-slot="select-root"
positioning={
positioning ?? { placement: "bottom-start", sameWidth: true }
}
{...rootProps}
>
{typeof children === "function" ? (
<SelectPrimitive.Context>
{(api) => children(api as UseSelectContext<T>)}
</SelectPrimitive.Context>
) : (
children
)}
<SelectPrimitive.HiddenSelect />
</SelectPrimitive.Root>
);
};
export const SelectControl = ({
className,
...props
}: SelectPrimitive.ControlProps) => (
<SelectPrimitive.Control
className={cn("flex w-full min-w-0 flex-col gap-1.5", className)}
{...props}
/>
);
export const SelectTrigger = ({ ...props }: SelectPrimitive.TriggerProps) => (
<SelectPrimitive.Trigger data-slot="select-trigger" {...props} />
);
export type SelectValueProps<T extends CollectionItem = CollectionItem> = {
children?: (api: UseSelectContext<T>) => ReactNode;
placeholder?: string;
className?: string;
};
export const SelectValue = <T extends CollectionItem = CollectionItem>({
children,
placeholder,
className,
}: SelectValueProps<T>) => (
<SelectPrimitive.Context>
{(api) => {
if (children) {
return (
<span
{...api.getValueTextProps()}
className={cn("min-w-0 flex-1 truncate text-left", className)}
data-slot="select-value"
>
{children(api as UseSelectContext<T>)}
</span>
);
}
const text = api.valueAsString?.trim();
return (
<SelectPrimitive.ValueText
placeholder={placeholder}
className={cn(
"min-w-0 flex-1 truncate text-left",
!text && "text-muted-foreground",
className,
)}
data-slot="select-value"
/>
);
}}
</SelectPrimitive.Context>
);
export type SelectPopupProps = SelectPrimitive.ContentProps & {
disablePortal?: boolean;
};
export const SelectPopup = ({
className,
disablePortal,
...props
}: SelectPopupProps) => {
const inner = (
<SelectPrimitive.Content
className={cn(
"outline-none flex max-h-[min(var(--available-height,20rem),20rem)] flex-col overflow-y-auto overscroll-contain rounded-lg border border-border bg-popover p-0.5 text-popover-foreground shadow-md",
!disablePortal && "z-50",
className,
)}
data-slot="select-content"
{...props}
/>
);
return disablePortal ? (
inner
) : (
<Portal>
<SelectPrimitive.Positioner>{inner}</SelectPrimitive.Positioner>
</Portal>
);
};
export type SelectItemProps = SelectPrimitive.ItemProps;
export const SelectItem = ({ className, ...props }: SelectItemProps) => (
<SelectPrimitive.Item
className={cn(
"relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground",
className,
)}
data-slot="select-item"
{...props}
/>
);
export const SelectItemText = ({
className,
...props
}: SelectPrimitive.ItemTextProps) => (
<SelectPrimitive.ItemText
className={cn("min-w-0 flex-1 truncate", className)}
data-slot="select-item-text"
{...props}
/>
);
export const SelectItemIndicator = ({
className,
children,
...props
}: SelectPrimitive.ItemIndicatorProps) => (
<SelectPrimitive.ItemIndicator
className={cn("text-primary", className)}
data-slot="select-item-indicator"
{...props}
>
{children ?? <CheckIcon className="size-4" />}
</SelectPrimitive.ItemIndicator>
);
export const SelectItemGroup = ({
className,
...props
}: SelectPrimitive.ItemGroupProps) => (
<SelectPrimitive.ItemGroup
className={cn("flex flex-col gap-0.5", className)}
data-slot="select-item-group"
{...props}
/>
);
export const SelectItemGroupLabel = ({
className,
...props
}: SelectPrimitive.ItemGroupLabelProps) => (
<SelectPrimitive.ItemGroupLabel
className={cn(
"px-2 py-1 font-medium text-muted-foreground text-xs uppercase tracking-wide",
className,
)}
data-slot="select-item-group-label"
{...props}
/>
);
export const SelectSeparator = ({
className,
...props
}: ComponentProps<"div">) => (
<div
role="separator"
className={cn("-mx-0.5 my-1 h-px bg-border", className)}
data-slot="select-separator"
{...props}
/>
);
export const SelectLabel = ({
className,
...props
}: SelectPrimitive.LabelProps) => (
<SelectPrimitive.Label
className={cn(
"mb-1 block font-medium text-foreground text-sm select-none data-disabled:opacity-50",
className,
)}
data-slot="select-label"
{...props}
/>
);
export const SelectIndicator = ({
className,
children,
...props
}: SelectPrimitive.IndicatorProps) => (
<SelectPrimitive.Indicator
className={cn("shrink-0 text-muted-foreground", className)}
data-slot="select-indicator"
{...props}
>
{children ?? <ChevronDownIcon className="size-4 opacity-80" aria-hidden />}
</SelectPrimitive.Indicator>
);
export type SelectClearTriggerProps = SelectPrimitive.ClearTriggerProps;
export const SelectClearTrigger = ({ ...props }: SelectClearTriggerProps) => (
<SelectPrimitive.ClearTrigger {...props} />
);
export type SelectTriggerFieldProps = {
showClear?: boolean;
className?: string;
children?: ReactNode;
hideIndicator?: boolean;
size?: VariantProps<typeof buttonVariants>["size"];
containerClass?: string;
};
export const SelectTriggerField = ({
showClear = false,
className,
children,
hideIndicator,
size = "default",
containerClass,
...props
}: SelectTriggerFieldProps) => (
<SelectPrimitive.Control asChild>
<ButtonGroup className={cn("w-full min-w-0", containerClass)}>
<SelectTrigger {...props} asChild>
<Button
variant={"outline"}
size={size}
className={cn("flex-1", className)}
>
{children}
{showClear ? (
<>
<SelectClearTrigger asChild>
<span role="button">
<XIcon />
</span>
</SelectClearTrigger>
</>
) : null}
{!hideIndicator && (
<SelectIndicator className="inline-flex items-center justify-center" />
)}
</Button>
</SelectTrigger>
</ButtonGroup>
</SelectPrimitive.Control>
);
export const SelectList = ({
className,
...props
}: SelectPrimitive.ListProps) => (
<SelectPrimitive.List
className={cn("flex flex-col gap-0.5 p-0.5 outline-none", className)}
data-slot="select-list"
{...props}
/>
);
export type SelectGroupedListProps<T extends CollectionItem = CollectionItem> =
{
className?: string;
items: readonly T[];
children: (group: readonly [string, T[]]) => ReactNode;
};
export const SelectGroupedList = <T extends CollectionItem = CollectionItem>({
items,
className,
children,
}: SelectGroupedListProps<T>) => {
// `useSelectContext()` is not generic over `T`; collection matches `T` when used under `<Select<T>>`.
const { collection } = useSelectContext();
const typedCollection = collection as ListCollection<T>;
const groups = typedCollection.group();
return (
<div
className={cn("flex flex-col gap-1 p-0.5 outline-none", className)}
data-slot="select-grouped-list"
>
{groups.map((tuple: readonly [string, (typeof items)[number][]]) => (
<Fragment key={tuple[0]}>{children(tuple)}</Fragment>
))}
</div>
);
};
export const SelectEmpty = ({ className, ...props }: ComponentProps<"div">) => {
const { collection } = useSelectContext();
if (collection.size > 0) {
return null;
}
return (
<div
role="presentation"
className={cn(
"rounded-md px-2 py-3 text-center text-muted-foreground text-sm",
className,
)}
data-slot="select-empty"
{...props}
/>
);
};
export const SelectContext = SelectPrimitive.Context;
export type SelectItemContextProps = SelectPrimitive.ItemContextProps;
export const SelectItemContext = SelectPrimitive.ItemContext;
Update import aliases to match your project setup.
Usage
import * as Select from "@/components/ui/select"Read exported parts in src/components/ui/select.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectBasic = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Select framework"
defaultValue={["next"]}
items={frameworks}
>
<SelectTriggerField>
<SelectValue placeholder="Select framework" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectBasic;
Controlled
value: vite
Next.js
Vite
Astro
import { useState } from "react";
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectControlled = () => {
const [value, setValue] = useState<string[]>(["vite"]);
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<p className="text-muted-foreground text-xs">
value:{" "}
<span className="font-medium text-foreground">
{value.join(", ") || "—"}
</span>
</p>
<Select
aria-label="Select framework"
items={frameworks}
value={value}
onValueChange={(details) => setValue(details.value)}
>
<SelectTriggerField>
<SelectValue placeholder="Select framework" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
};
export default SelectControlled;
Root provider
Next.js
Vite
Astro
import { Button } from "@/components/ui/button";
import {
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectLabel,
SelectList,
SelectPopup,
SelectRootProvider,
SelectTriggerField,
SelectValue,
useListCollection,
useSelect,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectRootProviderDemo = () => {
const { collection } = useListCollection({ initialItems: frameworks });
const store = useSelect({ collection });
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<SelectRootProvider value={store}>
<SelectLabel>Framework</SelectLabel>
<SelectTriggerField>
<SelectValue placeholder="Pick one" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</SelectRootProvider>
<Button
onClick={() => store.setValue(["astro"])}
type="button"
variant="outline"
>
Set to Astro
</Button>
</div>
);
};
export default SelectRootProviderDemo;
Multiple + select all
C++
C#
Go
Java
JavaScript
PHP
Python
Rust
Swift
TypeScript
import { Button } from "@/components/ui/button";
import {
Select,
SelectContext,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const labels: Record<string, string> = {
cpp: "C++",
csharp: "C#",
go: "Go",
java: "Java",
javascript: "JavaScript",
php: "PHP",
python: "Python",
rust: "Rust",
swift: "Swift",
typescript: "TypeScript",
};
const keys = Object.keys(labels);
const SelectMultiple = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Select languages"
defaultValue={["javascript", "typescript"]}
items={keys.map((value) => ({ label: labels[value] ?? value, value }))}
multiple
>
<SelectTriggerField>
<SelectValue>
{(api) => {
const selected = api.value;
if (selected.length === 0) {
return "Select languages…";
}
const first = selected[0] ? labels[selected[0]] : "";
const rest =
selected.length > 1 ? ` (+${selected.length - 1} more)` : "";
return first + rest;
}}
</SelectValue>
</SelectTriggerField>
<SelectPopup>
<SelectContext>
{(api) => (
<>
<div className="border-border border-b p-1">
<Button
className="h-8 w-full justify-start px-2 text-xs"
onClick={() => api.selectAll()}
type="button"
variant="ghost"
>
Select all
</Button>
</div>
<SelectList>
{keys.map((value) => {
const item = { label: labels[value] ?? value, value };
return (
<SelectItem key={value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
);
})}
</SelectList>
</>
)}
</SelectContext>
</SelectPopup>
</Select>
</div>
);
export default SelectMultiple;
Grouping
Frontend
Next.js
Vite
Astro
Backend
Express
NestJS
Fastify
import {
Select,
SelectGroupedList,
SelectItem,
SelectItemGroup,
SelectItemGroupLabel,
SelectItemIndicator,
SelectItemText,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frontend = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const backend = [
{ label: "Express", value: "express" },
{ label: "NestJS", value: "nestjs" },
{ label: "Fastify", value: "fastify" },
] as const;
const withGroups = [...frontend, ...backend];
const SelectGrouping = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Grouped frameworks"
groupBy={(item) =>
frontend.some((x) => x.value === item.value) ? "Frontend" : "Backend"
}
items={withGroups}
>
<SelectTriggerField>
<SelectValue placeholder="Select stack" />
</SelectTriggerField>
<SelectPopup>
<SelectGroupedList items={withGroups}>
{([group, groupItems]) => (
<SelectItemGroup key={group}>
<SelectItemGroupLabel>{group}</SelectItemGroupLabel>
{groupItems.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectItemGroup>
)}
</SelectGroupedList>
</SelectPopup>
</Select>
</div>
);
export default SelectGrouping;
Field
Next.js
Vite
Astro
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectFieldDemo = () => (
<Field className="max-w-xs gap-3">
<FieldLabel>Framework</FieldLabel>
<Select
aria-label="Framework field"
defaultValue={["next"]}
items={frameworks}
>
<SelectTriggerField>
<SelectValue placeholder="Choose…" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
<FieldDescription>Pick one for the starter template.</FieldDescription>
</Field>
);
export default SelectFieldDemo;
Form
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectFormDemo = () => {
const [submitted, setSubmitted] = useState<string | null>(null);
return (
<form
className="flex max-w-xs flex-col gap-3"
onSubmit={(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
setSubmitted(String(formData.get("framework") ?? ""));
}}
>
<Select
aria-label="Framework"
defaultValue={["next"]}
items={frameworks}
name="framework"
required
>
<SelectTriggerField>
<SelectValue placeholder="Select a framework" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
<Button type="submit">Submit</Button>
{submitted ? (
<p className="text-muted-foreground text-xs">
framework:{" "}
<span className="font-medium text-foreground">{submitted}</span>
</p>
) : null}
</form>
);
};
export default SelectFormDemo;
Async load
Open the menu to load the full list.
Next.js
import { useState } from "react";
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectAsyncDemo = () => {
const [items, setItems] = useState<(typeof frameworks)[number][]>(
frameworks.slice(0, 1),
);
const [busy, setBusy] = useState(false);
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<p className="text-muted-foreground text-xs">
Open the menu to load the full list{busy ? "…" : "."}
</p>
<Select
aria-label="Async frameworks"
items={items}
onOpenChange={({ open }) => {
if (open && items.length < frameworks.length) {
setBusy(true);
window.setTimeout(() => {
setItems([...frameworks]);
setBusy(false);
}, 500);
}
}}
>
<SelectTriggerField>
<SelectValue placeholder="Open to load…" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{items.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
};
export default SelectAsyncDemo;
Lazy mount
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectLazyMountDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Lazy select"
defaultValue={["next"]}
items={frameworks}
lazyMount
unmountOnExit
>
<SelectTriggerField>
<SelectValue />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectLazyMountDemo;
Overflow
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Item 13
Item 14
Item 15
Item 16
Item 17
Item 18
Item 19
Item 20
Item 21
Item 22
Item 23
Item 24
Item 25
Item 26
Item 27
Item 28
Item 29
Item 30
Item 31
Item 32
Item 33
Item 34
Item 35
Item 36
Item 37
Item 38
Item 39
Item 40
Item 41
Item 42
Item 43
Item 44
Item 45
Item 46
Item 47
Item 48
Item 49
Item 50
Item 51
Item 52
Item 53
Item 54
Item 55
Item 56
Item 57
Item 58
Item 59
Item 60
Item 61
Item 62
Item 63
Item 64
Item 65
Item 66
Item 67
Item 68
Item 69
Item 70
Item 71
Item 72
Item 73
Item 74
Item 75
Item 76
Item 77
Item 78
Item 79
Item 80
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const manyItems = Array.from({ length: 80 }, (_, index) => ({
label: `Item ${index + 1}`,
value: `item-${index + 1}`,
}));
const SelectOverflowDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Long list"
items={manyItems}
positioning={{
flip: true,
hideWhenDetached: true,
placement: "bottom-start",
sameWidth: true,
}}
>
<SelectTriggerField>
<SelectValue placeholder="Scroll the list" />
</SelectTriggerField>
<SelectPopup>
<SelectList className="max-h-52">
{manyItems.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectOverflowDemo;
Sizes
Next.js
Vite
Astro
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectSizesDemo = () => (
<div className="flex w-full max-w-md flex-col gap-4 sm:flex-row">
<Select aria-label="Small" defaultValue={["next"]} items={frameworks}>
<SelectTriggerField size="sm">
<SelectValue placeholder="Small" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
<Select aria-label="Large" defaultValue={["vite"]} items={frameworks}>
<SelectTriggerField size="lg">
<SelectValue placeholder="Large" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectSizesDemo;
Disabled
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectDisabledDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Disabled"
defaultValue={["next"]}
disabled
items={frameworks}
>
<SelectTriggerField>
<SelectValue placeholder="Framework" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectDisabledDemo;
Wider popup
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectSameWidthOffDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Wider popup"
defaultValue={["next"]}
items={frameworks}
positioning={{ placement: "bottom-start", sameWidth: false }}
>
<SelectTriggerField>
<SelectValue />
</SelectTriggerField>
<SelectPopup className="min-w-56">
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectSameWidthOffDemo;
With label
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectLabel,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const commandItems = [
{ description: "npx create-next-app", label: "Next.js", value: "next" },
{ description: "npm create vite@latest", label: "Vite", value: "vite" },
{ description: "npm create astro@latest", label: "Astro", value: "astro" },
] as const;
const SelectWithLabelDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Select command"
defaultValue={[commandItems[0].value]}
items={commandItems}
>
<SelectLabel>Commands</SelectLabel>
<SelectTriggerField>
<SelectValue />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{commandItems.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectWithLabelDemo;
Trigger icon
Next.js
Vite
Astro
import { CableIcon } from "lucide-react";
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectWithIconTriggerDemo = () => (
<div className="w-full max-w-xs">
<Select aria-label="With icon" defaultValue={["next"]} items={frameworks}>
<SelectTriggerField>
<CableIcon aria-hidden className="size-4 shrink-0 opacity-80" />
<SelectValue placeholder="Framework" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectWithIconTriggerDemo;
Options with icon
Components
Performance
Network
Development
import { Code2Icon, GlobeIcon, LayersIcon, ZapIcon } from "lucide-react";
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const iconOptions = [
{ icon: LayersIcon, label: "Components", value: "components" },
{ icon: ZapIcon, label: "Performance", value: "performance" },
{ icon: GlobeIcon, label: "Network", value: "network" },
{ icon: Code2Icon, label: "Development", value: "development" },
] as const;
const SelectOptionsWithIconDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Categories"
defaultValue={[iconOptions[0].value]}
items={iconOptions}
>
<SelectTriggerField>
<SelectValue>
{(api) => {
const item = api.selectedItems[0] as
| (typeof iconOptions)[number]
| undefined;
if (!item) {
return "Select…";
}
const Icon = item.icon;
return (
<span className="flex min-w-0 items-center gap-2">
<Icon className="size-4 shrink-0" />
<span className="truncate">{item.label}</span>
</span>
);
}}
</SelectValue>
</SelectTriggerField>
<SelectPopup>
<SelectList>
{iconOptions.map((item) => (
<SelectItem key={item.value} item={item}>
<span className="flex flex-1 items-center gap-2">
<item.icon className="size-4 shrink-0" />
<SelectItemText>{item.label}</SelectItemText>
</span>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectOptionsWithIconDemo;
Object values
Next.jsnpx create-next-app
Vitenpm create vite@latest
Astronpm create astro@latest
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const commandItems = [
{ description: "npx create-next-app", label: "Next.js", value: "next" },
{ description: "npm create vite@latest", label: "Vite", value: "vite" },
{ description: "npm create astro@latest", label: "Astro", value: "astro" },
] as const;
const SelectObjectValuesDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Commands"
defaultValue={[commandItems[0].value]}
itemToString={(item) => item.value}
items={commandItems}
>
<SelectTriggerField className="h-max min-h-8 sm:h-max">
<SelectValue>
{(api) => {
const item = api.selectedItems[0] as
| (typeof commandItems)[number]
| undefined;
if (!item) {
return "Select…";
}
return (
<span className="flex flex-col text-left">
<span className="truncate">{item.label}</span>
<span className="truncate text-muted-foreground text-xs">
{item.description}
</span>
</span>
);
}}
</SelectValue>
</SelectTriggerField>
<SelectPopup>
<SelectList>
{commandItems.map((item) => (
<SelectItem key={item.value} item={item}>
<span className="flex flex-1 flex-col gap-0.5">
<SelectItemText>{item.label}</SelectItemText>
<span className="text-muted-foreground text-xs">
{item.description}
</span>
</span>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectObjectValuesDemo;
Clear (ButtonGroup)
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectClearFieldDemo = () => (
<div className="w-full">
<Select aria-label="Clearable" defaultValue={["next"]} items={frameworks}>
<SelectTriggerField showClear>
<SelectValue placeholder="Framework" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectClearFieldDemo;
Invalid
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectInvalidDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Invalid"
defaultValue={["next"]}
invalid
items={frameworks}
>
<SelectTriggerField>
<SelectValue />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectInvalidDemo;
Read-only
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectReadOnlyDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Read-only"
defaultValue={["vite"]}
items={frameworks}
readOnly
>
<SelectTriggerField>
<SelectValue />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectReadOnlyDemo;
Deselectable
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectDeselectableDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Deselectable"
defaultValue={["next"]}
deselectable
items={frameworks}
>
<SelectTriggerField>
<SelectValue placeholder="Tap again to clear" />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectDeselectableDemo;
Empty
No items to display
import {
Select,
SelectEmpty,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const SelectEmptyDemo = () => (
<div className="w-full max-w-xs">
<Select items={[]}>
<SelectTriggerField>
<SelectValue placeholder="No items" />
</SelectTriggerField>
<SelectPopup>
<SelectEmpty>No items to display</SelectEmpty>
</SelectPopup>
</Select>
</div>
);
export default SelectEmptyDemo;
useSelectContext
useSelectContext: Next.js
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
useSelectContext,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectContextHookDemo = () => {
const Inner = () => {
const ctx = useSelectContext();
return (
<p className="text-muted-foreground text-xs">
useSelectContext:{" "}
<span className="font-medium text-foreground">
{ctx.valueAsString || "—"}
</span>
</p>
);
};
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<Select aria-label="Context" defaultValue={["next"]} items={frameworks}>
<Inner />
<SelectTriggerField>
<SelectValue />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
};
export default SelectContextHookDemo;
closeOnSelect false
Next.js
Vite
Astro
import {
Select,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTriggerField,
SelectValue,
} from "@/components/ui/select";
const frameworks = [
{ label: "Next.js", value: "next" },
{ label: "Vite", value: "vite" },
{ label: "Astro", value: "astro" },
] as const;
const SelectCloseOnSelectDemo = () => (
<div className="w-full max-w-xs">
<Select
aria-label="Stay open"
closeOnSelect={false}
defaultValue={["next"]}
items={frameworks}
>
<SelectTriggerField>
<SelectValue />
</SelectTriggerField>
<SelectPopup>
<SelectList>
{frameworks.map((item) => (
<SelectItem key={item.value} item={item}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
</div>
);
export default SelectCloseOnSelectDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
Select
| Prop | Type | Description |
|---|---|---|
| children? | ReactNode | ((api: UseSelectContext<T>) => ReactNode) | Render-prop or static children. |
| items? | T[] | List of items when not passing a custom collection. |
| collection? | ListCollection<T> | Pre-built collection (alternative to items). |
SelectTriggerField
| Prop | Type | Description |
|---|---|---|
| size? | ButtonSize | Outline trigger button size. |
| showClear? | boolean | Shows a clear control in the trigger. |
| hideIndicator? | boolean | Hides the dropdown indicator. |
| containerClass? | string | Class on the wrapping ButtonGroup. |
SelectPopup
| Prop | Type | Description |
|---|---|---|
| disablePortal? | boolean | Renders content in-place instead of portaled. |
SelectValue
| Prop | Type | Description |
|---|---|---|
| children? | (api: UseSelectContext<T>) => ReactNode | Custom value rendering. |
| placeholder? | string | Empty-state label. |
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.