A styled Radix Select with origin-aware scale-and-fade enter, groups with labels, and a check indicator on the selected item.
npx shadcn@latest add @paragon/select"use client";
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { cn } from "@/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
function SelectTrigger({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
className={cn(
"flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-input bg-transparent px-3 text-sm whitespace-nowrap text-foreground",
"transition-[border-color,box-shadow] duration-150 ease-out",
"outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/25",
"disabled:cursor-not-allowed disabled:opacity-50",
"data-[placeholder]:text-muted-foreground [&>span]:min-w-0 [&>span]:truncate",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown
aria-hidden
className="size-4 shrink-0 text-muted-foreground"
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
sideOffset = 4,
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<>
{/* Hoisted outside the Portal: React 19 keeps a hoistable <style> as a
child node, and the Radix select Portal enforces a single child. */}
<style href="paragon-select" precedence="paragon">{`
@keyframes paragon-select-in {
from { opacity: 0; scale: 0.97; filter: blur(2px); }
}
@keyframes paragon-select-out {
to { opacity: 0; scale: 0.99; }
}
@media (prefers-reduced-motion: reduce) {
[data-slot="select-content"] { animation: none !important; }
}
`}</style>
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
position={position}
sideOffset={sideOffset}
className={cn(
"relative z-50 min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-overlay",
"[transform-origin:var(--radix-select-content-transform-origin)]",
"data-[state=open]:[animation:paragon-select-in_180ms_var(--ease-out)]",
"data-[state=closed]:[animation:paragon-select-out_100ms_var(--ease-exit)_forwards]",
position === "popper" &&
"max-h-[min(24rem,var(--radix-select-content-available-height))] w-full min-w-[var(--radix-select-trigger-width)]",
className,
)}
{...props}
>
<SelectPrimitive.ScrollUpButton className="flex h-6 cursor-default items-center justify-center text-muted-foreground">
<ChevronUp aria-hidden className="size-4" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport className="p-1">
{children}
</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton className="flex h-6 cursor-default items-center justify-center text-muted-foreground">
<ChevronDown aria-hidden className="size-4" />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn(
"px-2 pt-1.5 pb-1 text-xs font-medium text-muted-foreground",
className,
)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-sm outline-none select-none",
"focus:bg-accent focus:text-accent-foreground",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<span className="absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check aria-hidden className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
}
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};