@cf/ui

@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

PropTypeDefault
SelectContent sidetop | right | bottom | left | inline-start | inline-endbottom
SelectContent alignstart | center | endcenter
SelectContent sideOffsetnumber4
SelectContent alignItemWithTriggerbooleantrue

Rules

The same rules ship in DESIGN.md and the MCP server, for the agents building with this.

  • must

    Give SelectTrigger an accessible name with a FieldLabel or an aria-label when there's no visible label.

    name-the-trigger

  • must

    Render SelectValue inside SelectTrigger, and pass items to Select so the trigger can show the chosen item's label instead of its raw value.

    value-inside-trigger

  • must

    Put SelectLabel inside a SelectGroup; it's Base UI's group label and has nothing to label on its own.

    labels-inside-groups

  • should

    Use DropdownMenu when the options are actions, and Select only when they are values.

    menu-for-actions

Install

bunx shadcn@latest add @cf/select

Needs the @cf registry in your components.json once. The theme and any sibling components come along automatically.

Source

select.tsxShow
"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,
};