Shadcn Select for React and Tailwind
Displays a list of options.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/select.jsonpnpm dlx shadcn@latest add https://kit.dev/r/select.jsonnpx shadcn@latest add https://kit.dev/r/select.jsonyarn shadcn@latest add https://kit.dev/r/select.json<Step>This component depends on Separator and Input. Install them first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-react tailwind-variantspnpm add @ark-ui/react lucide-react tailwind-variantsnpm install @ark-ui/react lucide-react tailwind-variantsyarn add @ark-ui/react lucide-react tailwind-variantsCopy and paste the following code into your project.
"use client";
import { Portal } from "@ark-ui/react/portal";
import { ark } from "@ark-ui/react/factory";
import { Select as ArkSelect, useSelectContext } from "@ark-ui/react/select";
import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
import type React from "react";
import type { VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils";
import { inputVariants } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
export const useSelect = useSelectContext;
export const SelectContext = ArkSelect.Context;
export const Select: ArkSelect.RootComponent = (props) => {
const { lazyMount = true, unmountOnExit = true, children, ...rest } = props;
return (
<ArkSelect.Root
data-slot="select"
lazyMount={lazyMount}
unmountOnExit={unmountOnExit}
{...rest}
>
{children}
<ArkSelect.HiddenSelect />
</ArkSelect.Root>
);
};
interface SelectTriggerProps
extends React.ComponentProps<typeof ArkSelect.Trigger>,
VariantProps<typeof inputVariants> {
/**
* Show clear trigger
*
* @default false
*/
showClear?: boolean;
}
export const SelectTrigger = (props: SelectTriggerProps) => {
const {
showClear = false,
size = "md",
className,
children,
...rest
} = props;
return (
<ArkSelect.Control data-slot="select-control">
<ArkSelect.Trigger
className={cn(
inputVariants({ size }),
"w-fit",
"flex items-center gap-2",
"text-sm",
"data-placeholder-shown:text-muted-foreground/64",
"data-[state=open]:border-primary data-[state=open]:ring-[3px] data-[state=open]:ring-ring/32",
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground",
className
)}
data-slot="select-trigger"
{...rest}
>
{children}
<div className="ms-auto flex items-center gap-1 rtl:me-auto">
{showClear && (
<SelectClearTrigger>
<XIcon />
</SelectClearTrigger>
)}
<ArkSelect.Indicator data-slot="select-indicator">
<ChevronsUpDownIcon />
</ArkSelect.Indicator>
</div>
</ArkSelect.Trigger>
</ArkSelect.Control>
);
};
export const SelectSeparator = (
props: React.ComponentProps<typeof Separator>
) => {
const { className, ...rest } = props;
return (
<Separator
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
data-slot="select-separator"
{...rest}
/>
);
};
export const SelectValue = (
props: React.ComponentProps<typeof ArkSelect.ValueText>
) => {
const { className, ...rest } = props;
return (
<ArkSelect.ValueText
className={cn(
"min-w-0",
"flex items-center gap-2",
"truncate text-nowrap",
className
)}
{...rest}
/>
);
};
export const SelectContent = (
props: React.ComponentProps<typeof ArkSelect.Content>
) => {
const { className, ...rest } = props;
return (
<Portal>
<ArkSelect.Positioner data-slot="select-positioner">
<ArkSelect.Content
className={cn(
"z-50",
"relative",
"max-h-96 min-w-(--reference-width)",
"p-1",
"bg-popover",
"text-popover-foreground",
"rounded-xl border shadow-lg/5",
"origin-(--transform-origin)",
"outline-none",
"overflow-y-auto",
"duration-100",
"data-[state=open]:animate-in",
"data-[state=open]:fade-in-0",
"data-[state=open]:zoom-in-[98%]",
"data-[placement=bottom]:slide-in-from-top-2",
"data-[placement=left]:slide-in-from-end-2",
"data-[placement=right]:slide-in-from-start-2",
"data-[placement=top]:slide-in-from-bottom-2",
"motion-reduce:animate-none!",
className
)}
data-slot="select-content"
{...rest}
/>
</ArkSelect.Positioner>
</Portal>
);
};
interface SelectGroupProps
extends React.ComponentProps<typeof ArkSelect.ItemGroup> {
/**
* The heading of the group
*/
heading?: string | React.ReactNode;
}
export const SelectGroup = (props: SelectGroupProps) => {
const { heading, children, ...rest } = props;
return (
<ArkSelect.ItemGroup data-slot="select-group" {...rest}>
{!heading && <SelectGroupLabel>{heading}</SelectGroupLabel>}
{children}
</ArkSelect.ItemGroup>
);
};
export const SelectGroupLabel = (
props: React.ComponentProps<typeof ArkSelect.ItemGroupLabel>
) => {
const { className, ...rest } = props;
return (
<ArkSelect.ItemGroupLabel
className={cn(
"px-2 py-1.5",
"font-semibold text-muted-foreground text-xs",
className
)}
data-slot="select-group-label"
{...rest}
/>
);
};
export const SelectItem = (
props: React.ComponentProps<typeof ArkSelect.Item>
) => {
const { className, children, ...rest } = props;
return (
<ArkSelect.Item
className={cn(
"relative",
"w-full",
"py-1.5 ps-2 pe-8",
"flex items-center gap-2",
"select-none text-base md:text-sm",
"rounded-lg",
"cursor-default",
"outline-hidden",
"in-[[data-slot=select-content]:has([data-slot=select-group-label])]:ps-4",
"data-highlighted:bg-accent data-highlighted:text-accent-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-64",
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
"[&_svg:not([class*='size-'])]:size-4 [&_svg]:text-muted-foreground",
className
)}
data-slot="select-item"
{...rest}
>
<ArkSelect.ItemText
className="flex w-full flex-1 items-center gap-2"
data-slot="select-item-text"
>
{children}
</ArkSelect.ItemText>
<span className="absolute inset-e-2 flex size-4 items-center justify-center">
<ArkSelect.ItemIndicator data-slot="select-item-indicator">
<CheckIcon />
</ArkSelect.ItemIndicator>
</span>
</ArkSelect.Item>
);
};
export const SelectClearTrigger = (
props: React.ComponentProps<typeof ArkSelect.ClearTrigger>
) => {
const { className, ...rest } = props;
return (
<ArkSelect.ClearTrigger
aria-label="Clear selected value(s)"
className={cn(
"[&_svg]:pointer-events-none [&_svg]:size-3.5 [&_svg]:shrink-0 [&_svg]:text-muted-foreground",
"transition-opacity",
"opacity-64",
"outline-none focus-visible:opacity-100",
"hover:opacity-100",
"motion-reduce:transition-none!",
className
)}
data-slot="select-clear-trigger"
{...rest}
/>
);
};
export const SelectEmpty = (props: React.ComponentProps<typeof ark.div>) => {
const { className, ...rest } = props;
const { empty } = useSelectContext();
if (empty) {
return (
<ark.div
className={cn(
"px-2 py-1.5",
"text-center text-muted-foreground text-sm",
className
)}
role="presentation"
{...rest}
/>
);
}
return null;
};Update the import paths to match your project setup.
Anatomy
Select
├── SelectTrigger
│ └── SelectValue
└── SelectContent
├── SelectEmpty
├── SelectClearTrigger
├── SelectGroup
│ ├── SelectGroupLabel
│ └── SelectItem
└── SelectItemUsage
import { createListCollection } from "@ark-ui/react/collection";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";const collection = createListCollection({
items: ["Option 1", "Option 2", "Option 3"],
});
<Select collection={collection}>
<SelectTrigger>
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
{collection.items.map((item) => (
<SelectItem item={item} key={item}>
{item}
</SelectItem>
))}
</SelectContent>
</Select>Controlled
Use value and onValueChange to control the selected items.
Size
Control trigger size with the size prop on SelectTrigger.
Small
Medium
Large
States
Disabled
Invalid
Examples
Multiple
Enable multiple selection with the multiple prop.
Grouping
Organize options into groups using groupBy and SelectGroup.
With Field
Use Field for labels, helper text, and error handling.
Max Selection
Scrollable
Empty
Use SelectEmpty to display a message when the collection has no items.
API Reference
Select
Root component.
| Prop | Type | Default |
|---|---|---|
collection | ListCollection<T> | - |
value | string[] | - |
defaultValue | string[] | - |
onValueChange | (details: ValueChangeDetails<T>) => void | - |
multiple | boolean | false |
disabled | boolean | - |
invalid | boolean | - |
name | string | - |
required | boolean | - |
positioning | PositioningOptions | - |
lazyMount | boolean | true |
unmountOnExit | boolean | true |
className | string | - |
SelectTrigger
Opens the dropdown on click. Displays the selected value or placeholder.
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "md" |
showClear | boolean | false |
className | string | - |
asChild | boolean | - |
SelectValue
Shows the selected value or placeholder. Use SelectContext as children for custom display.
| Prop | Type | Default |
|---|---|---|
placeholder | string | - |
className | string | - |
asChild | boolean | - |
SelectContent
Holds the dropdown options. Displayed in a portal.
| Prop | Type | Default |
|---|---|---|
className | string | - |
asChild | boolean | - |
SelectClearTrigger
Button to clear the selected value. Shown when showClear is true on SelectTrigger.
| Prop | Type | Default |
|---|---|---|
className | string | - |
asChild | boolean | - |
SelectGroup
Groups related options under an optional heading.
| Prop | Type | Default |
|---|---|---|
heading | string | ReactNode | - |
className | string | - |
asChild | boolean | - |
SelectGroupLabel
Label for an option group.
| Prop | Type | Default |
|---|---|---|
className | string | - |
asChild | boolean | - |
SelectItem
A single selectable option in the dropdown.
| Prop | Type | Default |
|---|---|---|
item | T | - |
className | string | - |
asChild | boolean | - |
SelectEmpty
Shown when the collection has no items (collection.size === 0). Place inside SelectContent.
| Prop | Type | Default |
|---|---|---|
className | string | - |
For a complete list of props, see the Ark UI documentation.