Forms
Select
Choose one option from a list, on the same surface and states as Input.
pnpm dlx shadcn@latest add @paragon/selectBrings in@base-ui/reactlucide-reactcn
States
Same surface, sizes and states as Input. The chevron turns as the list opens beneath.
Placeholder
With icons
Invalid
Disabled
Sizes
registry/paragon/ui/select.tsx
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "cn"
const Select = SelectPrimitive.Root
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return <SelectPrimitive.Group data-slot="select-group" className={cn("scroll-my-1.5", className)} {...props} />
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return <SelectPrimitive.Value data-slot="select-value" className={cn("flex flex-1 text-start", className)} {...props} />
}
/** Shares Input's surface, sizes and states, so selects sit in forms without overrides. */
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default" | "lg"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"group/select-trigger flex w-fit min-w-0 items-center justify-between gap-2 rounded-button border border-input bg-card/70 text-start whitespace-nowrap text-foreground outline-none select-none",
"transition-[border-color,box-shadow,background-color] duration-(--duration-base) ease-paragon",
"hover:border-foreground/35",
"focus-visible:border-ring focus-visible:bg-card focus-visible:ring-4 focus-visible:ring-ring/15",
"data-popup-open:border-ring data-popup-open:bg-card data-popup-open:ring-4 data-popup-open:ring-ring/15",
"aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive/15 aria-invalid:data-popup-open:ring-destructive/15",
"data-disabled:pointer-events-none data-disabled:bg-secondary/60 data-disabled:opacity-55",
"data-placeholder:text-muted-foreground",
"data-[size=sm]:h-9 data-[size=sm]:rounded-[calc(var(--radius-button)-2px)] data-[size=sm]:ps-3 data-[size=sm]:pe-2.5 data-[size=sm]:text-sm",
"data-[size=default]:h-[2.625rem] data-[size=default]:ps-3.5 data-[size=default]:pe-3 data-[size=default]:text-base md:data-[size=default]:text-[0.9375rem]",
"data-[size=lg]:h-12 data-[size=lg]:ps-4 data-[size=lg]:pe-3.5 data-[size=lg]:text-base",
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
"dark:bg-input/15 dark:data-popup-open:bg-input/25 dark:focus-visible:bg-input/25",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground transition-transform duration-(--duration-base) ease-paragon group-data-popup-open/select-trigger:rotate-180" />
}
/>
</SelectPrimitive.Trigger>
)
}
/**
* Drops below the trigger by default. Set `alignItemWithTrigger` for the
* native-style overlay that lines the selected item up with the trigger.
*/
function SelectContent({
className,
children,
side = "bottom",
sideOffset = 6,
align = "start",
alignOffset = 0,
alignItemWithTrigger = false,
...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) min-w-44 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-card bg-popover p-1.5 text-popover-foreground ring-1 ring-border outline-none",
"shadow-[0_1px_2px_color-mix(in_oklab,var(--ink),transparent_92%),0_16px_36px_-12px_color-mix(in_oklab,var(--ink),transparent_78%)] dark:shadow-[0_16px_36px_-12px_rgb(0_0_0/0.65)]",
"duration-(--duration-base) ease-paragon",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
"data-[side=bottom]:slide-in-from-top-1 data-[side=top]:slide-in-from-bottom-1 data-[side=inline-end]:slide-in-from-start-1 data-[side=inline-start]:slide-in-from-end-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1",
"data-closed:animate-out data-closed:duration-(--duration-fast) data-closed:fade-out-0 data-closed:zoom-out-95",
"data-[align-trigger=true]:animate-none motion-reduce: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(
"px-2.5 pt-2 pb-1 text-[0.6875rem] font-medium tracking-[0.08em] text-muted-foreground uppercase",
className
)}
{...props}
/>
)
}
function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex min-h-9 w-full cursor-default items-center gap-2 rounded-[calc(var(--radius-button)-4px)] py-1.5 ps-2.5 pe-9 text-[0.9375rem] outline-hidden transition-colors duration-(--duration-fast) select-none",
"data-highlighted:bg-foreground/6 data-selected:font-medium",
"data-disabled:pointer-events-none data-disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
"*:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={<span className="pointer-events-none absolute end-2.5 flex size-4 items-center justify-center" />}
>
<CheckIcon className="pointer-events-none animate-in text-foreground! duration-(--duration-base) zoom-in-50 fade-in" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
}
function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1.5 my-1.5 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 text-muted-foreground [&_svg:not([class*='size-'])]:size-4",
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 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 text-muted-foreground [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownArrow>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}