Get Started
Input
Feedback
Display
Installation
i18n
import {
Select,
SelectContent,Installation#
pnpm dlx shadcn@latest add https://ui.tyap.me/r/styles/base/select.jsonnpx shadcn@latest add https://ui.tyap.me/r/styles/base/select.jsonyarn dlx shadcn@latest add https://ui.tyap.me/r/styles/base/select.jsonbunx --bun shadcn@latest add https://ui.tyap.me/r/styles/base/select.json
Install the following dependencies:
pnpm add @base-ui/reactnpm install @base-ui/reactyarn add @base-ui/reactbun add @base-ui/react
Copy and paste the following code into your project.
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { useIsMobile } from "@/hooks/use-mobile"
import { useShakeOnInvalid } from "@/hooks/use-shake-on-invalid"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
type SelectKind = "auto" | "native" | "custom"
function Select<Value = string, Multiple extends boolean | undefined = false>({
kind = "auto",
...props
}: SelectPrimitive.Root.Props<Value, Multiple> & { kind?: SelectKind }) {
const isMobile = useIsMobile()
const mode = kind === "auto" ? (isMobile ? "native" : "custom") : kind
if (mode === "native") {
// Native <select>/<option> projection only supports string values.
return <SelectNative {...(props as SelectPrimitive.Root.Props<string>)} />
}
return <SelectPrimitive.Root data-slot="select" {...props} />
}
// ----------------------------------------------------------------------------
// Native rendering — used when kind="native" (or kind="auto" on mobile).
// Re-uses the exact same JSX as the custom select: SelectTrigger / SelectValue /
// SelectContent / SelectGroup / SelectLabel / SelectItem children are
// introspected and projected onto a real <select> / <option> / <optgroup> tree.
// ----------------------------------------------------------------------------
function isElementOfType<P>(
node: React.ReactNode,
component: React.ComponentType<P>
): node is React.ReactElement<P> {
return React.isValidElement(node) && node.type === component
}
function nativeOptionsFromNodes(nodes: React.ReactNode): React.ReactNode {
return React.Children.map(nodes, (node) => {
if (isElementOfType(node, SelectItem)) {
const { value, children, disabled } = node.props
return (
<option value={value as string} disabled={disabled}>
{children}
</option>
)
}
if (isElementOfType(node, SelectGroup)) {
let label: React.ReactNode = undefined
const options: React.ReactNode[] = []
React.Children.forEach(node.props.children, (child) => {
if (isElementOfType(child, SelectLabel)) {
label = child.props.children
} else {
const projected = nativeOptionsFromNodes(child)
React.Children.forEach(projected, (o) => options.push(o))
}
})
return (
<optgroup label={typeof label === "string" ? label : undefined}>
{options}
</optgroup>
)
}
// SelectSeparator, SelectLabel outside a group, scroll buttons, etc. have no
// native equivalent and are dropped.
return null
})
}
function SelectNative({
children,
value,
defaultValue,
onValueChange,
name,
disabled,
required,
multiple,
}: SelectPrimitive.Root.Props<string>) {
const ref = React.useRef<HTMLDivElement>(null)
useShakeOnInvalid(ref)
// Pull trigger styling/sizing/attributes and the placeholder/options out of
// the children. Attributes the user put on <SelectTrigger> (id, aria-invalid,
// aria-describedby, …) belong on the real focusable <select> in native mode.
let triggerClassName: string | undefined
let size: "sm" | "default" = "default"
let triggerAttributes: Record<string, unknown> = {}
let placeholder: React.ReactNode = undefined
let options: React.ReactNode = null
React.Children.forEach(children, (child) => {
if (isElementOfType(child, SelectTrigger)) {
const {
className,
size: triggerSize,
children: triggerChildren,
render: _render,
...rest
} = child.props as SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}
triggerClassName = typeof className === "string" ? className : undefined
if (triggerSize) size = triggerSize
triggerAttributes = rest
React.Children.forEach(triggerChildren, (c) => {
if (isElementOfType(c, SelectValue)) {
placeholder = c.props.placeholder
}
})
} else if (isElementOfType(child, SelectContent)) {
options = nativeOptionsFromNodes(child.props.children)
}
})
const isControlled = value !== undefined
const hasInitialValue = isControlled || defaultValue !== undefined
return (
<div
ref={ref}
className={cn(
"t-input group/native-select relative w-fit has-[select:disabled]:opacity-50",
triggerClassName
)}
data-slot="native-select-wrapper"
data-size={size}
>
<select
data-slot="native-select"
data-size={size}
className="outline-none disabled:pointer-events-none disabled:cursor-not-allowed"
value={value as string | undefined}
defaultValue={
(defaultValue as string | undefined) ??
(placeholder != null && !hasInitialValue ? "" : undefined)
}
onChange={
onValueChange
? (event) => (onValueChange as (v: string) => void)(event.target.value)
: undefined
}
name={name}
disabled={disabled}
required={required}
multiple={multiple}
{...triggerAttributes}
>
{placeholder != null ? (
<option value="" disabled>
{placeholder}
</option>
) : null}
{options}
</select>
<ChevronDownIcon className="pointer-events-none absolute select-none" aria-hidden="true" data-slot="native-select-icon" />
</div>
)
}
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn(className)}
{...props}
/>
)
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn(className)}
{...props}
/>
)
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}) {
const ref = React.useRef<HTMLButtonElement>(null)
useShakeOnInvalid(ref)
return (
<SelectPrimitive.Trigger
ref={ref}
data-slot="select-trigger"
data-size={size}
className={cn(
"t-input flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDownIcon className="pointer-events-none" />
}
/>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
...props
}: SelectPrimitive.Popup.Props &
Pick<
SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", className )}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn(className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...props}
>
<SelectPrimitive.ItemText className="shrink-0 whitespace-nowrap">
{children}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator render={<span className=""><CheckIcon className="pointer-events-none" /></span>} />
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn("top-0 w-full", className)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpArrow>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn("bottom-0 w-full", className)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownArrow>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
Update the import paths to match your project setup.
Usage#
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"const items = [
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
{ label: "System", value: "system" },
]
<Select items={items}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>Composition#
Use the following composition to build a Select:
Select
├── SelectTrigger
│ └── SelectValue
└── SelectContent
├── SelectGroup
│ ├── SelectLabel
│ ├── SelectItem
│ └── SelectItem
├── SelectSeparator
└── SelectGroup
├── SelectLabel
├── SelectItem
└── SelectItemExamples#
Align Item With Trigger#
Use alignItemWithTrigger on SelectContent to control whether the selected item aligns with the trigger. When true (default), the popup positions so the selected item appears over the trigger. When false, the popup aligns to the trigger edge.
Toggle to align the item with the trigger.
"use client"
import * as React from "react"Groups#
Use SelectGroup, SelectLabel, and SelectSeparator to organize items.
import {
Select,
SelectContent,Scrollable#
A select with many items that scrolls.
import {
Select,
SelectContent,Disabled#
import {
Select,
SelectContent,Invalid#
Add the data-invalid attribute to the Field component and the aria-invalid attribute to the SelectTrigger component to show an error state.
<Field data-invalid>
<FieldLabel>Fruit</FieldLabel>
<SelectTrigger aria-invalid>
<SelectValue />
</SelectTrigger>
</Field>Please select a fruit.
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import {
Select,RTL#
To enable RTL support in shadcn/ui, see the RTL configuration guide.
"use client"
import * as React from "react"API Reference#
See the Base UI Select documentation.