@cf/select
Select
A single choice from a short, known list, opened from a trigger that shows the current value.
The second one turns alignItemWithTrigger off so the list drops below the trigger, which reads better once there are group labels.
Base UI's Select. The popup lines the selected item up with the trigger by default (alignItemWithTrigger), and items get the same touch treatment as menu rows: 16px text and taller padding below md.
The note editor's section picker is the real use. It restyles the trigger into a small borderless h-7 control, which is fine, but the trigger has no fixed height of its own, so anything you put beside it needs checking.
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
<Select items={items} onValueChange={setValue} value={value}>
<SelectTrigger aria-label="Section">
<SelectValue />
</SelectTrigger>
<SelectContent>
{items.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>Props
| Prop | Type | Default |
|---|---|---|
| SelectContent side | top | right | bottom | left | inline-start | inline-end | bottom |
| SelectContent align | start | center | end | center |
| SelectContent sideOffset | number | 4 |
| SelectContent alignItemWithTrigger | boolean | true |
Rules
The same rules ship in DESIGN.md and the MCP server, for the agents building with this.
- must
Give
SelectTriggeran accessible name with aFieldLabelor anaria-labelwhen there's no visible label.name-the-trigger
- must
Render
SelectValueinsideSelectTrigger, and passitemstoSelectso the trigger can show the chosen item's label instead of its raw value.value-inside-trigger
- must
Put
SelectLabelinside aSelectGroup; it's Base UI's group label and has nothing to label on its own.labels-inside-groups
- should
Use
DropdownMenuwhen the options are actions, and Select only when they are values.menu-for-actions
Install
bunx shadcn@latest add @cf/selectNeeds the @cf registry in your components.json once. The theme and any sibling components come along automatically.
Source
select.tsxShowHide
"use client";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { CaretDownIcon, CaretUpIcon, CheckIcon } from "@phosphor-icons/react";
import { cn } from "cn";
const Select = SelectPrimitive.Root;
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return <SelectPrimitive.Group className={cn("p-1", className)} {...props} />;
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
className={cn("flex flex-1 text-left", className)}
{...props}
/>
);
}
function SelectTrigger({
className,
children,
...props
}: SelectPrimitive.Trigger.Props) {
return (
<SelectPrimitive.Trigger
className={cn(
"flex w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-2 py-1 text-base shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm [&>span]:line-clamp-1 [&_svg]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon render={<CaretDownIcon className="opacity-50" />} />
</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" | "alignItemWithTrigger" | "side" | "sideOffset"
>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
align={align}
alignItemWithTrigger={alignItemWithTrigger}
alignOffset={alignOffset}
className="isolate z-50"
side={side}
sideOffset={sideOffset}
>
<SelectPrimitive.Popup
className={cn(
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-closed:animate-out data-open:animate-in",
className
)}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List className="p-1">
{children}
</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
className={cn("px-2 py-1.5 font-semibold text-sm", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-2.5 pr-8 pl-2 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-50 md:py-1.5 md:text-sm",
className
)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={
<span className="absolute right-2 flex size-3.5 items-center justify-center" />
}
>
<CheckIcon />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: SelectPrimitive.ScrollUpArrow.Props) {
return (
<SelectPrimitive.ScrollUpArrow
className={cn(
"flex cursor-default items-center justify-center py-1 [&_svg]:size-4",
className
)}
{...props}
>
<CaretUpIcon />
</SelectPrimitive.ScrollUpArrow>
);
}
function SelectScrollDownButton({
className,
...props
}: SelectPrimitive.ScrollDownArrow.Props) {
return (
<SelectPrimitive.ScrollDownArrow
className={cn(
"flex cursor-default items-center justify-center py-1 [&_svg]:size-4",
className
)}
{...props}
>
<CaretDownIcon />
</SelectPrimitive.ScrollDownArrow>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};