Forms

Select

An accessible single or multiple select with a quebi input-style trigger and a dropdown surface for options. Built on react-aria-components — foundational for Calendar and Conform Select.

selectforminputdropdownlistboxoptionsinteractive

Default

A basic single select with a placeholder and a list of options.

With label & description

Compose with the field Label and Description primitives.

PlanChoose the tier for your workspace.

Items with icons

Options can carry a leading icon alongside the label.

Descriptions

Each option can show a secondary line of context.

Sections, separator & intent

Group options under a titled section, divide them, and flag destructive items.

Disabled

The whole control dims and blocks interaction.

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { ChevronsUpDown } from "lucide-react"
import type {
  ListBoxProps,
  PopoverProps,
  SelectProps as SelectPrimitiveProps,
} from "react-aria-components"
import { Button, ListBox, Select as SelectPrimitive, SelectValue } from "react-aria-components"
import { cn } from "@/lib/utils"
import {
  DropdownDescription,
  DropdownItem,
  DropdownLabel,
  DropdownSection,
  DropdownSeparator,
} from "@/components/dropdown"
import { PopoverContent } from "@/components/popover"

/**
 * Select — quebi design system
 *
 * An accessible single/multiple select built on react-aria-components. The
 * trigger reuses the quebi input style (translucent field, cyan hairline,
 * brand-teal focus ring); the chevron is muted; the options reuse the dropdown
 * surface and items. Foundational — Calendar and Conform Select compose this.
 */

interface SelectProps<T extends object, M extends "single" | "multiple" = "single">
  extends SelectPrimitiveProps<T, M> {
  items?: Iterable<T, M>
}

const Select = <T extends object, M extends "single" | "multiple" = "single">({
  className,
  ...props
}: SelectProps<T, M>) => {
  return (
    <SelectPrimitive
      data-slot="control"
      className={cn("group/select w-full", className)}
      {...props}
    />
  )
}

interface SelectListProps<T extends object>
  extends Omit<ListBoxProps<T>, "layout" | "orientation"> {
  items?: Iterable<T>
  popover?: Omit<PopoverProps, "children">
}

const SelectContent = <T extends object>({
  items,
  className,
  popover,
  ...props
}: SelectListProps<T>) => {
  return (
    <PopoverContent
      placement={popover?.placement ?? "bottom"}
      className={cn(
        "min-w-(--trigger-width) scroll-py-1 overflow-y-auto overscroll-contain",
        popover?.className,
      )}
      {...popover}
    >
      <ListBox
        layout="stack"
        orientation="vertical"
        className={cn(
          "grid max-h-96 w-full grid-cols-[auto_1fr] flex-col gap-y-1 overflow-y-auto p-1 outline-hidden *:[[role='group']+[role=group]]:mt-4 *:[[role='group']+[role=separator]]:mt-1",
          className,
        )}
        items={items}
        {...props}
      />
    </PopoverContent>
  )
}

interface SelectTriggerProps extends React.ComponentProps<typeof Button> {
  prefix?: React.ReactNode
  className?: string
}

const SelectTrigger = ({ children, className, ...props }: SelectTriggerProps) => {
  return (
    <span data-slot="control" className="relative block w-full">
      <Button
        className={cn(
          // quebi input style — translucent field, cyan hairline, brand-teal focus.
          "group/select-trigger flex w-full min-w-0 cursor-default items-center gap-x-2 text-start text-sm text-white",
          "rounded-quebi-sm border border-cyan-500/20 bg-white/[0.02] px-3 py-2.5",
          "transition-[border-color,box-shadow] duration-200",
          "enabled:hover:border-cyan-500/40",
          // focus / open → brand-teal border + ring.
          "outline-none focus:outline-none focus:border-quebi-brand focus:ring-2 focus:ring-quebi-brand/50",
          "group-open/select:border-quebi-brand group-open/select:ring-2 group-open/select:ring-quebi-brand/50",
          // invalid wins via red border + ring.
          "group-invalid/select:border-red-500 group-invalid/select:focus:ring-red-500/50 group-invalid/select:group-open/select:ring-red-500/50",
          // leading icons / loader, muted.
          "*:data-[slot=icon]:size-4 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:self-center *:data-[slot=icon]:text-quebi-fg-muted",
          "*:data-[slot=loader]:size-4 *:data-[slot=loader]:shrink-0 *:data-[slot=loader]:self-center *:data-[slot=loader]:text-quebi-fg-muted",
          "group-disabled/select:cursor-not-allowed group-disabled/select:opacity-50",
          "in-disabled:opacity-50",
          className,
        )}
      >
        {(values) => (
          <>
            {props.prefix && <span className="text-quebi-fg-muted">{props.prefix}</span>}
            {typeof children === "function" ? children(values) : children}

            {!children && (
              <>
                <SelectValue
                  data-slot="select-value"
                  className={cn(
                    "truncate text-start text-sm data-placeholder:text-quebi-fg-subtle **:[[slot=description]]:hidden",
                    "has-data-[slot=avatar]:grid has-data-[slot=avatar]:grid-cols-[1fr_auto] has-data-[slot=avatar]:items-center has-data-[slot=avatar]:gap-x-2",
                    "has-data-[slot=icon]:grid has-data-[slot=icon]:grid-cols-[1fr_auto] has-data-[slot=icon]:items-center has-data-[slot=icon]:gap-x-2",
                    "*:data-[slot=icon]:size-4",
                    "*:mt-0 *:data-[slot=avatar]:[--avatar-size:--spacing(4)]",
                  )}
                />
                <ChevronsUpDown
                  data-slot="chevron"
                  className="ms-auto -me-1 size-4 shrink-0 text-quebi-fg-muted"
                />
              </>
            )}
          </>
        )}
      </Button>
    </span>
  )
}

const SelectSection = DropdownSection
const SelectSeparator = DropdownSeparator
const SelectLabel = DropdownLabel
const SelectDescription = DropdownDescription
const SelectItem = DropdownItem

export type { SelectProps, SelectTriggerProps }
export {
  Select,
  SelectContent,
  SelectDescription,
  SelectItem,
  SelectLabel,
  SelectSection,
  SelectSeparator,
  SelectTrigger,
}