Select
A picker for one value from a list, on the input's field and the menu's panel.
pnpm exec shadcn add @intelligo/selectInside an app made by intelligo create, or any shadcn project that names the registry.
used by these pages
more in primitives
Alert dialog, Button, Dialog, Dropdown menu, Input, Popover, Progress, Sheet, Sidebar, Tabs, Textarea, Tooltip, Checkbox, Radio group, Switch, Collapsible, Command
Source
What shadcn add writes to components/ui/select.tsx. It is yours from then on.
"use client";
/*
* The select: the input's field as a trigger and the menu's panel as its
* list. The list springs open with motion; the root is kept controlled so
* the exit plays.
*/
import * as React from "react";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { popupMotion, useOpenState } from "@showcase/components/ui/ai-motion";
import { cn } from "@showcase/lib/utils";
// A select's popup mounts on first open and then stays in the DOM, so the
// content animates with presence until then and with open state after.
type SelectPresence = {
open: boolean;
mounted: boolean;
markMounted: () => void;
};
const SelectPresenceContext = React.createContext<SelectPresence | null>(
null
);
function Select<Value, Multiple extends boolean | undefined = false>({
open: openProp,
defaultOpen,
onOpenChange,
...props
}: SelectPrimitive.Root.Props<Value, Multiple>) {
const [open, setOpen] = useOpenState(openProp, defaultOpen, onOpenChange);
const [mounted, setMounted] = React.useState(false);
const markMounted = React.useCallback(() => setMounted(true), []);
const presence = React.useMemo(
() => ({ open, mounted, markMounted }),
[open, mounted, markMounted]
);
return (
<SelectPresenceContext.Provider value={presence}>
<SelectPrimitive.Root open={open} onOpenChange={setOpen} {...props} />
</SelectPresenceContext.Provider>
);
}
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
);
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("flex flex-1 text-left", className)}
{...props}
/>
);
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-card py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-[border-color,box-shadow] duration-normal ease-standard hover:border-ring/50 outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_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" />
}
/>
</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"
>) {
const presence = React.useContext(SelectPresenceContext);
const open = presence?.open ?? true;
const mounted = presence?.mounted ?? false;
const markMounted = presence?.markMounted;
const positionerRef = React.useCallback(() => markMounted?.(), [markMounted]);
const reduced = useReducedMotion() ?? false;
// Laid over the trigger, the list only fades; below it, it grows.
const motionProps = popupMotion(reduced, alignItemWithTrigger ? 1 : 0.92);
return (
<AnimatePresence>
{(open || mounted) && (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
ref={positionerRef}
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-popover"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
render={
mounted ? (
<motion.div
initial={false}
animate={open ? motionProps.animate : motionProps.exit}
/>
) : (
<motion.div {...motionProps} />
)
}
className={cn(
"relative isolate max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg border border-border bg-popover text-popover-foreground shadow-lg",
className
)}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)}
</AnimatePresence>
);
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-2 py-1 text-xs text-muted-foreground", 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 gap-1.5 rounded-lg py-1.5 pr-8 pl-2 text-sm outline-hidden transition-colors duration-fast select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[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 right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</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 my-1 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 [&_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 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownArrow>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};Ready to try it? Three commands and the app runs against a stub model.