Search

Jump to any page.

Forms

Select

Choose one option from a list, on the same surface and states as Input.

pnpm dlx shadcn@latest add @paragon/select
Brings 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,
}