Overview
Get Started
Select
A common form component for choosing a predefined value in a dropdown menu.
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ label: "Select a framework", value: null },
{ label: "Next.js", value: "next" },
{ label: "Tanstack start", value: "tanstack start" },
{ label: "Astro", value: "astro", disabled: true },
];
export function SelectDemo() {
return (
<Select items={items}>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup>
{items.map(({ label, value, disabled }) => (
<SelectItem key={value} value={value} disabled={!!disabled}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
);
}Installation
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { cn } from "@/lib/cn";
function Select(props: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default" | "lg";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
className={cn(
"inline-flex h-9 w-full select-none items-center justify-between gap-2 rounded-lg border border-input px-3 text-left text-sm shadow-xs outline-none transition-[color,shadow] hover:bg-input/30 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 data-disabled:pointer-events-none data-disabled:opacity-60 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
"aria-invalid:border-danger aria-invalid:ring-danger/30",
"[&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
size === "sm" && "h-8 gap-1.5 px-2.5",
size === "lg" && "h-10",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon data-slot="select-icon">
<IconChevronDown className="-me-1" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectValue({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn(
"line-clamp-1 flex-1 truncate data-placeholder:text-muted-foreground",
className
)}
{...props}
/>
);
}
function SelectPositioner({
className,
align = "start",
side = "bottom",
sideOffset = 4,
alignItemWithTrigger = false,
...props
}: React.ComponentProps<typeof SelectPrimitive.Positioner>) {
return (
<SelectPrimitive.Positioner
data-slot="select-positioner"
align={align}
side={side}
sideOffset={sideOffset}
alignItemWithTrigger={alignItemWithTrigger}
className={cn("z-50 min-w-(--anchor-width) select-none", className)}
{...props}
/>
);
}
function SelectPopup({
className,
children,
nativeScroll = false,
showArrow = false,
positionerProps,
...props
}: React.ComponentProps<typeof SelectPrimitive.Popup> & {
nativeScroll?: boolean;
showArrow?: boolean;
positionerProps?: React.ComponentProps<typeof SelectPositioner>;
}) {
return (
<SelectPrimitive.Portal>
<SelectPositioner {...positionerProps}>
<SelectPrimitive.Popup
data-slot="select-popup"
className="origin-(--transform-origin) overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-md transition-[scale,opacity] has-data-[side=none]:scale-100 has-data-starting-style:scale-98 has-data-starting-style:opacity-0 has-data-[side=none]:transition-none"
{...props}
>
{showArrow && (
<SelectPrimitive.Arrow
data-slot="tooltip-arrow"
className="flex transition duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=top]:-bottom-2 data-[side=right]:-left-3.25 data-[side=bottom]:rotate-0 data-[side=left]:rotate-90 data-[side=right]:-rotate-90 data-[side=top]:rotate-180 data-instant:transition-none"
>
<ArrowSvg />
</SelectPrimitive.Arrow>
)}
{!nativeScroll && (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-arrow"
className="top-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:top-px before:h-[200%] before:rounded-t-[calc(var(--radius-lg)-1px)] before:bg-linear-to-b before:from-50% before:from-popover"
>
<IconChevronUp className="relative size-4.5 sm:size-4" />
</SelectPrimitive.ScrollUpArrow>
)}
<SelectPrimitive.List
className={cn("max-h-(--available-height) overflow-y-auto p-1", className)}
data-slot="select-list"
>
{children}
</SelectPrimitive.List>
{!nativeScroll && (
<SelectPrimitive.ScrollDownArrow
className="bottom-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:bottom-px before:h-[200%] before:rounded-b-[calc(var(--radius-lg)-1px)] before:bg-linear-to-t before:from-50% before:from-popover"
data-slot="select-scroll-down-arrow"
>
<IconChevronDown className="relative size-4" />
</SelectPrimitive.ScrollDownArrow>
)}
</SelectPrimitive.Popup>
</SelectPositioner>
</SelectPrimitive.Portal>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"grid min-h-8 cursor-pointer select-none grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-sm outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-60 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}
>
<SelectPrimitive.ItemIndicator data-slot="select-item-indicator" className="col-start-1">
<IconCheck />
</SelectPrimitive.ItemIndicator>
<SelectPrimitive.ItemText className="col-start-2 min-w-0">
{children}
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
function SelectGroup(props: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectGroupLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.GroupLabel>) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-group-label"
className={cn("px-2 py-1.5 font-medium text-muted-foreground text-xs", className)}
{...props}
/>
);
}
export function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="fill-popover"
/>
{/* <path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
className="fill-border"
/> */}
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="fill-border"
/>
</svg>
);
}
export {
Select,
SelectTrigger,
SelectValue,
SelectPositioner,
SelectPopup,
SelectItem,
SelectSeparator,
SelectGroup,
SelectGroupLabel,
};Usage
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";<Select>
<SelectTrigger className="w-45">
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectItem value={null}>Theme</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectPopup>
</Select>Examples
Sizes
Three sizes are available: sm, md (default) and lg.
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ label: "Select a framework", value: null },
{ label: "Next.js", value: "next" },
{ label: "Tanstack start", value: "tanstack start" },
{ label: "Astro", value: "astro" },
];
export function SelectDemo() {
return (
<div className="flex flex-col gap-5">
<Select items={items}>
<SelectTrigger size="sm" className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup>
{items.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
<Select items={items}>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup>
{items.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
<Select items={items}>
<SelectTrigger size="lg" className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup>
{items.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
</div>
);
}Disabled
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ label: "Select a framework", value: null },
{ label: "Next.js", value: "next" },
{ label: "Tanstack start", value: "tanstack start" },
{ label: "Astro", value: "astro" },
];
export function SelectDemo() {
return (
<Select items={items} disabled>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup>
{items.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
);
}Align item with trigger
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ label: "Select a framework", value: null },
{ label: "Next.js", value: "next" },
{ label: "Tanstack start", value: "tanstack start" },
{ label: "Astro", value: "astro" },
];
export function SelectDemo() {
return (
<Select items={items}>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup positionerProps={{ alignItemWithTrigger: true }}>
{items.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
);
}With groups
import {
Select,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectPopup,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const placeholder = [{ label: "Select framework", value: null }];
const frontend = [
{ label: "Next.js", value: "next" },
{ label: "Tanstack Start", value: "tanstack start" },
{ label: "Astro", value: "astro" },
];
const backend = [
{ label: "Express", value: "express" },
{ label: "NestJS", value: "nestjs" },
{ label: "Hono", value: "hono" },
{ label: "Elysia", value: "elysia" },
{ label: "Gin", value: "gin" },
{ label: ".NET", value: "dotnet" },
];
export function SelectDemo() {
return (
<Select aria-label="Select framework" items={[...placeholder, ...frontend, ...backend]}>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectGroup>
<SelectGroupLabel>Frontend</SelectGroupLabel>
{frontend.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectGroupLabel>Backend</SelectGroupLabel>
{backend.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
</SelectPopup>
</Select>
);
}Multiple selection
Enable multiple selection by passing the multiple prop to the Select component.
"use client";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const languages = {
cpp: "C++",
csharp: "C#",
go: "Go",
java: "Java",
javascript: "JavaScript",
php: "PHP",
python: "Python",
rust: "Rust",
swift: "Swift",
typescript: "TypeScript",
} as const;
type Language = keyof typeof languages;
const values = Object.keys(languages) as Language[];
function renderValue(value: Language[]) {
if (value.length === 0) {
return "Select languages…";
}
const firstLanguage = value[0] ? languages[value[0]] : "";
const additionalLanguages = value.length > 1 ? ` (+${value.length - 1} more)` : "";
return firstLanguage + additionalLanguages;
}
export function SelectDemo() {
return (
<Select aria-label="Select languages" defaultValue={["javascript", "typescript"]} multiple>
<SelectTrigger className="w-64">
<SelectValue>{renderValue}</SelectValue>
</SelectTrigger>
<SelectPopup>
{values.map((value) => (
<SelectItem key={value} value={value}>
{languages[value]}
</SelectItem>
))}
</SelectPopup>
</Select>
);
}With icon
import { IconClock } from "@tabler/icons-react";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ label: "Select time", value: null },
{ label: "Morning", value: "morning" },
{ label: "Afternoon", value: "afternoon" },
{ label: "Evening", value: "evening" },
{ label: "Night", value: "night" },
];
export function SelectDemo() {
return (
<Select items={items}>
<SelectTrigger className="w-64">
<IconClock />
<SelectValue />
</SelectTrigger>
<SelectPopup>
{items.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
);
}With options icon
"use client";
import {
IconBrandAngular,
IconBrandReact,
IconBrandSolidjs,
IconBrandVue,
IconPackage,
} from "@tabler/icons-react";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ icon: IconPackage, label: "Select library", value: null },
{ icon: IconBrandReact, label: "React", value: "react" },
{ icon: IconBrandAngular, label: "Angular", value: "angular" },
{ icon: IconBrandSolidjs, label: "SolidJS", value: "solidjs" },
{ icon: IconBrandVue, label: "Vue", value: "vue" },
];
type Item = (typeof items)[number];
export function SelectDemo() {
return (
<Select
aria-label="Select category"
defaultValue={items[0]}
itemToStringValue={(item) => (item as Item).value ?? ""}
>
<SelectTrigger className="w-64">
<SelectValue>
{(item: Item) => (
<span className="flex items-center gap-2">
<item.icon />
<span className="truncate">{item.label}</span>
</span>
)}
</SelectValue>
</SelectTrigger>
<SelectPopup>
{items.map((item) => (
<SelectItem key={item.value} value={item}>
<span className="flex items-center gap-2">
<item.icon />
<span className="truncate">{item.label}</span>
</span>
</SelectItem>
))}
</SelectPopup>
</Select>
);
}With object values
"use client";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ description: "Select project size", label: "Select size", value: null },
{ description: "Small size projects", label: "Small", value: "small" },
{ description: "Medium size projects", label: "Medium", value: "medium" },
{ description: "Large size projects", label: "Large", value: "large" },
{ description: "Enterprise size projects", label: "Enterprise", value: "enterprise" },
];
type Item = (typeof items)[number];
export function SelectDemo() {
return (
<Select
aria-label="Select framework with command"
defaultValue={items[0]}
itemToStringValue={(item) => (item as Item).value ?? ""}
>
<SelectTrigger className="h-12 w-64">
<SelectValue>
{(item: Item) => (
<span className="flex flex-col">
<span className="truncate">{item.label}</span>
<span className="truncate text-muted-foreground text-xs">{item.description}</span>
</span>
)}
</SelectValue>
</SelectTrigger>
<SelectPopup>
{items.map((item) => (
<SelectItem key={item.value} value={item}>
<span className="flex flex-col">
<span className="truncate">{item.label}</span>
<span className="truncate text-muted-foreground text-xs">{item.description}</span>
</span>
</SelectItem>
))}
</SelectPopup>
</Select>
);
}With scrolling
By default, the Select component uses built-in scrollbars. You can also enable native scrolling by passing nativeScroll prop to the SelectPopup component.
Built-in scrollbars (default):
Native scrollbars:
import {
Select,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectPopup,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const placeholder = [{ label: "Select framework", value: null }];
const language = [
{ label: "JavaScript", value: "javascript" },
{ label: "TypeScript", value: "typescript" },
{ label: "Python", value: "python" },
{ label: "Go", value: "go" },
{ label: "C#", value: "csharp" },
{ label: "Ruby", value: "ruby" },
{ label: "Java", value: "java" },
{ label: "PHP", value: "php" },
];
const frontend = [
{ label: "Next.js", value: "next" },
{ label: "Tanstack Start", value: "tanstack start" },
{ label: "Astro", value: "astro" },
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
];
const backend = [
{ label: "Express", value: "express" },
{ label: "NestJS", value: "nestjs" },
{ label: "Hono", value: "hono" },
{ label: "Elysia", value: "elysia" },
{ label: "Gin", value: "gin" },
{ label: ".NET", value: "dotnet" },
{ label: "Django", value: "django" },
{ label: "Flask", value: "flask" },
{ label: "Spring", value: "spring" },
{ label: "Laravel", value: "laravel" },
];
export function SelectDemo() {
return (
<div className="flex flex-col gap-5">
<div>
<div className="mb-1">Built-in scrollbars (default):</div>
<Select aria-label="Select tech stack" items={[...placeholder, ...frontend, ...backend]}>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectGroup>
<SelectGroupLabel>Language</SelectGroupLabel>
{language.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectGroupLabel>Frontend</SelectGroupLabel>
{frontend.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectGroupLabel>Backend</SelectGroupLabel>
{backend.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
</SelectPopup>
</Select>
</div>
<div>
<div className="mb-1">Native scrollbars:</div>
<Select aria-label="Select tech stack" items={[...placeholder, ...frontend, ...backend]}>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup nativeScroll>
<SelectGroup>
<SelectGroupLabel>Language</SelectGroupLabel>
{language.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectGroupLabel>Frontend</SelectGroupLabel>
{frontend.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectGroupLabel>Backend</SelectGroupLabel>
{backend.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectGroup>
</SelectPopup>
</Select>
</div>
</div>
);
}Controlled
Selected value: null
"use client";
import { useState } from "react";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ label: "Select a framework", value: null },
{ label: "Next.js", value: "next" },
{ label: "Tanstack start", value: "tanstack start" },
{ label: "Astro", value: "astro" },
];
export function SelectDemo() {
const [value, setValue] = useState<string | null>(null);
return (
<div className="flex flex-col gap-5">
<Select
items={items}
value={value}
onValueChange={(value) => setValue(value as string | null)}
>
<SelectTrigger className="w-64">
<SelectValue />
</SelectTrigger>
<SelectPopup>
{items.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
<div>Selected value: {value ?? "null"}</div>
</div>
);
}