# Select

> Pick one or more options from a list that opens on the current value, with typeahead, groups and form support.

Docs: https://hextaui.com/docs/select
Markdown: https://hextaui.com/docs/select.md

```tsx title="components/examples/select/demo.tsx"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const fonts = [
  { value: "inter", label: "Inter" },
  { value: "geist", label: "Geist" },
  { value: "ibm-plex", label: "IBM Plex Sans" },
  { value: "source-serif", label: "Source Serif" },
  { value: "jetbrains", label: "JetBrains Mono" },
]

export function SelectDemo() {
  return (
    <Select items={fonts} defaultValue="geist">
      <SelectTrigger aria-label="Font" className="w-48">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        {fonts.map((font) => (
          <SelectItem key={font.value} value={font.value}>
            {font.label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/select.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/select.tsx"
"use client"

import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import {
  IconCheck,
  IconChevronDown,
  IconChevronUp,
  IconSelector,
} from "@tabler/icons-react"
import { cva } from "class-variance-authority"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const SelectTriggerContext =
  React.createContext<React.RefObject<HTMLElement | null> | null>(null)

function Select<Value, Multiple extends boolean | undefined = false>(
  props: SelectPrimitive.Root.Props<Value, Multiple>
) {
  const triggerRef = React.useRef<HTMLElement | null>(null)

  return (
    <SelectTriggerContext.Provider value={triggerRef}>
      <SelectPrimitive.Root {...props} />
    </SelectTriggerContext.Provider>
  )
}

function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
  return (
    <SelectPrimitive.Group
      data-slot="select-group"
      className={mergeClassName("flex flex-col", className)}
      {...props}
    />
  )
}

function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
  return (
    <SelectPrimitive.Value
      data-slot="select-value"
      className={mergeClassName(
        "flex min-w-0 flex-1 items-center gap-2 truncate text-start",
        className
      )}
      {...props}
    />
  )
}

const selectTriggerVariants = cva(
  "group/select-trigger inline-flex w-fit min-w-0 items-center justify-between gap-2 rounded-md bg-transparent text-sm whitespace-nowrap inset-ring-(length:--hairline) inset-ring-input transition-[color,background-color,box-shadow] duration-150 ease-out-cubic outline-none select-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:inset-ring-destructive aria-invalid:focus-visible:ring-destructive/70 data-invalid:inset-ring-destructive data-placeholder:text-muted-foreground data-popup-open:inset-ring-ring dark:bg-input/30 dark:hover:bg-input/50 forced-colors:border pointer-coarse:text-[max(16px,1rem)] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [@media(hover:hover)]:hover:bg-muted/50",
  {
    variants: {
      size: {
        sm: "h-8 ps-2.5 pe-1.5",
        default: "h-9 ps-3 pe-2",
        lg: "h-10 ps-3 pe-2.5",
      },
    },
    defaultVariants: {
      size: "default",
    },
  }
)

type SelectTriggerProps = SelectPrimitive.Trigger.Props & {
  size?: "sm" | "default" | "lg"
}

function SelectTrigger({
  className,
  size = "default",
  children,
  ref,
  ...props
}: SelectTriggerProps) {
  const triggerRef = React.useContext(SelectTriggerContext)
  const setRef = React.useCallback(
    (node: HTMLButtonElement | null) => {
      if (triggerRef) {
        triggerRef.current = node
      }
      if (typeof ref === "function") {
        return ref(node)
      }
      if (ref) {
        ref.current = node
      }
      return undefined
    },
    [triggerRef, ref]
  )

  return (
    <SelectPrimitive.Trigger
      ref={setRef}
      data-slot="select-trigger"
      data-size={size}
      className={mergeClassName(selectTriggerVariants({ size }), className)}
      {...props}
    >
      {children}
      <SelectPrimitive.Icon
        data-slot="select-icon"
        className="flex text-muted-foreground transition-colors group-data-popup-open/select-trigger:text-foreground"
      >
        <IconSelector />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  )
}

type SelectContentProps = SelectPrimitive.Popup.Props &
  Pick<
    SelectPrimitive.Positioner.Props,
    "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
  >

function SelectContent({
  className,
  children,
  side = "bottom",
  sideOffset = 6,
  align = "start",
  alignOffset = 0,
  alignItemWithTrigger = true,
  dir,
  ...props
}: SelectContentProps) {
  const triggerRef = React.useContext(SelectTriggerContext)
  const setPositioner = React.useCallback(
    (positioner: HTMLDivElement | null) => {
      const trigger = triggerRef?.current
      if (!positioner || dir !== undefined || !trigger?.isConnected) {
        return
      }
      if (getComputedStyle(trigger).direction === "rtl") {
        positioner.setAttribute("dir", "rtl")
      } else {
        positioner.removeAttribute("dir")
      }
    },
    [triggerRef, dir]
  )

  return (
    <SelectPrimitive.Portal>
      <SelectPrimitive.Positioner
        ref={setPositioner}
        dir={dir}
        data-slot="select-positioner"
        side={side}
        sideOffset={sideOffset}
        align={align}
        alignOffset={alignOffset}
        alignItemWithTrigger={alignItemWithTrigger}
        className="isolate z-50 outline-none select-none focus-visible:outline-hidden"
      >
        <SelectPrimitive.Popup
          data-slot="select-content"
          className={mergeClassName(
            "group/select-content relative isolate w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-(length:--hairline) ring-foreground/10 outline-none [--select-item-radius:max(calc(var(--radius-sm)*0.5),calc(var(--radius-lg)-0.25rem))] focus-visible:outline-hidden forced-colors:border",
            className
          )}
          {...props}
        >
          <SelectScrollUpButton />
          <SelectPrimitive.List
            data-slot="select-list"
            className="relative max-h-(--available-height) scroll-py-7 overflow-y-auto overscroll-none p-1 outline-none focus-visible:outline-hidden"
          >
            {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={mergeClassName(
        "px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

function SelectItem({
  className,
  children,
  ...props
}: SelectPrimitive.Item.Props) {
  return (
    <SelectPrimitive.Item
      data-slot="select-item"
      className={mergeClassName(
        "group/select-item relative flex min-h-8 w-full cursor-default items-center gap-2 rounded-(--select-item-radius) py-1.5 ps-2 pe-7 text-start text-sm outline-none select-none focus-visible:outline-hidden data-highlighted:bg-accent data-highlighted:text-accent-foreground forced-colors:data-highlighted:outline-2 forced-colors:data-highlighted:-outline-offset-2 forced-colors:data-highlighted:outline-solid pointer-coarse:min-h-11 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",
        className
      )}
      {...props}
    >
      <SelectPrimitive.ItemText
        data-slot="select-item-text"
        className="flex min-w-0 flex-1 items-center gap-2 wrap-anywhere"
      >
        {children}
      </SelectPrimitive.ItemText>
      <SelectPrimitive.ItemIndicator
        keepMounted
        data-slot="select-item-indicator"
        className="absolute end-2 flex size-4 items-center justify-center opacity-0 transition-[opacity,scale] duration-150 ease-out-quint data-[selected]:opacity-100 motion-safe:scale-50 motion-safe:data-[selected]:scale-100 motion-reduce:transition-none"
      >
        <IconCheck className="text-foreground" />
      </SelectPrimitive.ItemIndicator>
    </SelectPrimitive.Item>
  )
}

function SelectSeparator({
  className,
  ...props
}: SelectPrimitive.Separator.Props) {
  return (
    <SelectPrimitive.Separator
      data-slot="select-separator"
      className={mergeClassName(
        "pointer-events-none -mx-1 my-1 h-px bg-border",
        className
      )}
      {...props}
    />
  )
}

const scrollArrowClassName =
  "absolute inset-x-0 z-1 flex h-7 cursor-default items-center justify-center text-muted-foreground [&_svg:not([class*='size-'])]:size-4"

function SelectScrollUpButton({
  className,
  ...props
}: SelectPrimitive.ScrollUpArrow.Props) {
  return (
    <SelectPrimitive.ScrollUpArrow
      data-slot="select-scroll-up-button"
      className={mergeClassName(
        cn(
          scrollArrowClassName,
          "top-0 rounded-t-[inherit] bg-linear-to-b from-popover from-55% to-transparent"
        ),
        className
      )}
      {...props}
    >
      <IconChevronUp />
    </SelectPrimitive.ScrollUpArrow>
  )
}

function SelectScrollDownButton({
  className,
  ...props
}: SelectPrimitive.ScrollDownArrow.Props) {
  return (
    <SelectPrimitive.ScrollDownArrow
      data-slot="select-scroll-down-button"
      className={mergeClassName(
        cn(
          scrollArrowClassName,
          "bottom-0 rounded-b-[inherit] bg-linear-to-t from-popover from-55% to-transparent"
        ),
        className
      )}
      {...props}
    >
      <IconChevronDown />
    </SelectPrimitive.ScrollDownArrow>
  )
}

export {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectScrollDownButton,
  SelectScrollUpButton,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
  selectTriggerVariants,
}
export type { SelectContentProps, SelectTriggerProps }
```

```ts title="lib/motion.ts"
import * as React from "react"

const easeOut = "cubic-bezier(0.23, 1, 0.32, 1)"
const easeInOut = "cubic-bezier(0.77, 0, 0.175, 1)"
const easeSpring =
  "linear(0, 0.015 2%, 0.0532 4%, 0.1065 6%, 0.1686 8%, 0.2351 10%, 0.3363 13%, 0.4332 16%, 0.5495 20%, 0.648 24%, 0.7287 28%, 0.807 33%, 0.8646 38%, 0.9128 44%, 0.9447 50%, 0.968 57%, 0.9834 65%, 0.9924 74%, 0.9975 85%, 1)"

const duration = {
  press: 100,
  release: 200,
  hover: 150,
  enter: 200,
  exit: 150,
  morph: 300,
} as const

function prefersReducedMotion() {
  return (
    typeof window === "undefined" ||
    typeof window.matchMedia !== "function" ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches
  )
}

type SizeAxis = "width" | "height"

type SizeMorphOptions = {
  axis: SizeAxis
  enabled?: boolean
  duration?: number
  easing?: string
}

function readSize(element: HTMLElement, axis: SizeAxis) {
  const value = parseFloat(getComputedStyle(element)[axis])
  if (Number.isFinite(value)) {
    return value
  }
  const rect = element.getBoundingClientRect()
  return axis === "width" ? rect.width : rect.height
}

function attachSizeMorph(
  element: HTMLElement,
  axis: SizeAxis,
  time: number,
  easing: string
) {
  if (
    typeof MutationObserver === "undefined" ||
    typeof element.animate !== "function"
  ) {
    return undefined
  }

  let settled = readSize(element, axis)
  let animation: Animation | null = null

  const morph = () => {
    const from = animation ? readSize(element, axis) : settled
    animation?.cancel()
    animation = null
    const to = readSize(element, axis)
    settled = to

    if (
      Math.abs(from - to) < 0.5 ||
      prefersReducedMotion() ||
      !element.isConnected ||
      element.getClientRects().length === 0
    ) {
      element.removeAttribute("data-morphing")
      return
    }

    element.setAttribute("data-morphing", "")
    const running = element.animate(
      [{ [axis]: `${from}px` }, { [axis]: `${to}px` }],
      { duration: time, easing }
    )
    animation = running
    running.onfinish = () => {
      if (animation === running) {
        animation = null
        element.removeAttribute("data-morphing")
        settled = readSize(element, axis)
      }
    }
  }

  const mutations = new MutationObserver(morph)
  mutations.observe(element, {
    childList: true,
    subtree: true,
    characterData: true,
  })

  const resize =
    typeof ResizeObserver === "undefined"
      ? null
      : new ResizeObserver(() => {
          if (!animation) {
            settled = readSize(element, axis)
          }
        })
  resize?.observe(element)

  return () => {
    mutations.disconnect()
    resize?.disconnect()
    animation?.cancel()
    element.removeAttribute("data-morphing")
  }
}

function useSizeMorph<T extends HTMLElement>({
  axis,
  enabled = true,
  duration: time = duration.morph,
  easing = easeOut,
}: SizeMorphOptions): React.RefCallback<T> {
  return React.useCallback(
    (element: T | null) => {
      if (!element || !enabled) {
        return undefined
      }
      return attachSizeMorph(element, axis, time, easing)
    },
    [axis, enabled, time, easing]
  )
}

function useSlidingHighlight(
  barRef: React.RefObject<HTMLElement | null>,
  highlightRef: React.RefObject<HTMLElement | null>,
  selector: string,
  attribute = "data-popup-open"
) {
  React.useLayoutEffect(() => {
    const bar = barRef.current
    const highlight = highlightRef.current
    if (!bar || !highlight) {
      return
    }

    let current: HTMLElement | null = null

    const place = (trigger: HTMLElement, instant: boolean) => {
      if (instant || prefersReducedMotion()) {
        highlight.setAttribute("data-instant", "")
      } else {
        highlight.removeAttribute("data-instant")
      }
      const frame = bar.getBoundingClientRect()
      const box = trigger.getBoundingClientRect()
      const scale = bar.offsetWidth > 0 ? frame.width / bar.offsetWidth : 1
      highlight.style.left = "0px"
      highlight.style.width = `${box.width / scale}px`
      highlight.style.height = `${box.height / scale}px`
      highlight.style.transform = `translate(${(box.left - frame.left) / scale - bar.clientLeft}px, ${(box.top - frame.top) / scale - bar.clientTop}px)`
    }

    const sync = () => {
      const trigger = bar.querySelector<HTMLElement>(selector)
      if (trigger === current) {
        if (trigger) {
          place(trigger, true)
        }
        return
      }
      const appearing = current === null
      current = trigger
      if (!trigger) {
        highlight.removeAttribute("data-visible")
        return
      }
      place(trigger, appearing)
      if (appearing) {
        void highlight.offsetWidth
      }
      highlight.setAttribute("data-visible", "")
    }

    sync()
    const mutations = new MutationObserver(sync)
    mutations.observe(bar, {
      subtree: true,
      childList: true,
      attributes: true,
      attributeFilter: [attribute],
    })
    const resize =
      typeof ResizeObserver === "undefined"
        ? null
        : new ResizeObserver(() => {
            if (current) {
              place(current, true)
            }
          })
    resize?.observe(bar)
    const onScroll = () => {
      if (current) {
        place(current, true)
      }
    }
    bar.addEventListener("scroll", onScroll, { capture: true, passive: true })

    return () => {
      mutations.disconnect()
      resize?.disconnect()
      bar.removeEventListener("scroll", onScroll, { capture: true })
    }
  }, [barRef, highlightRef, selector, attribute])
}

export {
  duration,
  easeInOut,
  easeOut,
  easeSpring,
  prefersReducedMotion,
  useSizeMorph,
  useSlidingHighlight,
}
export type { SizeMorphOptions }
```

Update the import paths to match your project setup.

## Usage

```tsx
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
```

```tsx
<Select items={fonts} defaultValue="geist">
  <SelectTrigger aria-label="Font">
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    {fonts.map((font) => (
      <SelectItem key={font.value} value={font.value}>
        {font.label}
      </SelectItem>
    ))}
  </SelectContent>
</Select>
```

The list opens right over the trigger with the current option lined up on top of the value, so your eye never loses its place. Pass `items` so `SelectValue` shows labels instead of raw values. For long lists that need searching, use Combobox.

## Composition

```text
Select
├── SelectTrigger
│   └── SelectValue
└── SelectContent
    ├── SelectGroup
    │   ├── SelectLabel
    │   └── SelectItem
    └── SelectSeparator
```

## Examples

### Sizes

`size` on `SelectTrigger` matches Input and Button heights.

```tsx title="components/examples/select/sizes.tsx"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const sizes = ["sm", "default", "lg"] as const

export function SelectSizes() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {sizes.map((size) => (
        <Select key={size} defaultValue="week">
          <SelectTrigger size={size} aria-label={`Range (${size})`}>
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="day">Today</SelectItem>
            <SelectItem value="week">This week</SelectItem>
            <SelectItem value="month">This month</SelectItem>
          </SelectContent>
        </Select>
      ))}
    </div>
  )
}
```

### Groups and long lists

Group options with `SelectGroup` and `SelectLabel`. Long lists fit the screen and show scroll arrows that scroll when you hover them.

```tsx title="components/examples/select/groups.tsx"
import {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const zones = [
  {
    label: "Americas",
    items: [
      "Los Angeles",
      "Denver",
      "Chicago",
      "New York",
      "Toronto",
      "Mexico City",
      "Bogotá",
      "São Paulo",
      "Buenos Aires",
    ],
  },
  {
    label: "Europe",
    items: ["London", "Lisbon", "Paris", "Berlin", "Stockholm", "Athens"],
  },
  {
    label: "Asia",
    items: ["Dubai", "Mumbai", "Singapore", "Shanghai", "Tokyo", "Seoul"],
  },
]

export function SelectGroups() {
  return (
    <Select defaultValue="Berlin">
      <SelectTrigger aria-label="Time zone" className="w-56">
        <SelectValue />
      </SelectTrigger>
      <SelectContent>
        {zones.map((zone, index) => (
          <SelectGroup key={zone.label}>
            {index > 0 ? <SelectSeparator /> : null}
            <SelectLabel>{zone.label}</SelectLabel>
            {zone.items.map((city) => (
              <SelectItem key={city} value={city}>
                {city}
              </SelectItem>
            ))}
          </SelectGroup>
        ))}
      </SelectContent>
    </Select>
  )
}
```

### With icons

Put icons in items, and pass a function to `SelectValue` to show the same icon in the trigger.

```tsx title="components/examples/select/icons.tsx"
"use client"

import {
  IconCircleCheck,
  IconCircleDashed,
  IconCircleHalf2,
  IconCircleX,
} from "@tabler/icons-react"

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const statuses = [
  { value: "backlog", label: "Backlog", icon: IconCircleDashed },
  { value: "progress", label: "In progress", icon: IconCircleHalf2 },
  { value: "done", label: "Done", icon: IconCircleCheck },
  { value: "canceled", label: "Canceled", icon: IconCircleX },
]

export function SelectIcons() {
  return (
    <Select defaultValue="progress">
      <SelectTrigger aria-label="Status" className="w-44">
        <SelectValue>
          {(value: string) => {
            const status = statuses.find((item) => item.value === value)
            if (!status) {
              return null
            }
            const Icon = status.icon
            return (
              <>
                <Icon />
                {status.label}
              </>
            )
          }}
        </SelectValue>
      </SelectTrigger>
      <SelectContent>
        {statuses.map((status) => (
          <SelectItem key={status.value} value={status.value}>
            <status.icon />
            {status.label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}
```

### Multiple

With `multiple`, the list stays open while you pick, and the value can summarise long selections.

```tsx title="components/examples/select/multiple.tsx"
"use client"

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const labels = ["Bug", "Feature", "Design", "Docs", "Performance"]

export function SelectMultiple() {
  return (
    <Select multiple defaultValue={["Bug", "Design"]}>
      <SelectTrigger aria-label="Labels" className="w-56">
        <SelectValue placeholder="Add labels">
          {(value: string[]) =>
            value.length > 2 ? `${value.length} labels` : value.join(", ")
          }
        </SelectValue>
      </SelectTrigger>
      <SelectContent alignItemWithTrigger={false}>
        {labels.map((label) => (
          <SelectItem key={label} value={label}>
            {label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  )
}
```

### In a form

Inside `Field`, the trigger gets its label, description and required validation.

```tsx title="components/examples/select/field.tsx"
"use client"

import { Form } from "@base-ui/react/form"

import { Button } from "@/components/ui/button"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/components/ui/field"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const roles = [
  { value: "viewer", label: "Viewer" },
  { value: "editor", label: "Editor" },
  { value: "admin", label: "Admin" },
]

export function SelectField() {
  return (
    <Form
      className="flex w-full max-w-xs flex-col items-start gap-4"
      onSubmit={(event) => event.preventDefault()}
    >
      <Field name="role">
        <FieldLabel>Role</FieldLabel>
        <Select items={roles} required>
          <SelectTrigger className="w-full">
            <SelectValue placeholder="Choose a role" />
          </SelectTrigger>
          <SelectContent>
            {roles.map((role) => (
              <SelectItem key={role.value} value={role.value}>
                {role.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>
        <FieldDescription>Admins can invite people.</FieldDescription>
        <FieldError match="valueMissing">Choose a role to continue.</FieldError>
      </Field>
      <Button type="submit" size="sm">
        Invite
      </Button>
    </Form>
  )
}
```

### Disabled and invalid

Disable the whole select or single options, and mark it invalid with aria-invalid.

```tsx title="components/examples/select/states.tsx"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export function SelectStates() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Select defaultValue="pro" disabled>
        <SelectTrigger aria-label="Plan" className="w-36">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="pro">Pro</SelectItem>
        </SelectContent>
      </Select>
      <Select defaultValue="weekly">
        <SelectTrigger aria-label="Digest" className="w-36">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="daily">Daily</SelectItem>
          <SelectItem value="weekly">Weekly</SelectItem>
          <SelectItem value="monthly" disabled>
            Monthly (soon)
          </SelectItem>
        </SelectContent>
      </Select>
      <Select>
        <SelectTrigger aria-label="Region" aria-invalid className="w-36">
          <SelectValue placeholder="Region" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="us">United States</SelectItem>
          <SelectItem value="eu">Europe</SelectItem>
        </SelectContent>
      </Select>
    </div>
  )
}
```

### Below the trigger

`alignItemWithTrigger={false}` opens the list under the trigger like a menu. Touch input does this automatically.

```tsx title="components/examples/select/dropdown.tsx"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

export function SelectDropdown() {
  return (
    <Select defaultValue="newest">
      <SelectTrigger aria-label="Sort by" className="w-44">
        <SelectValue />
      </SelectTrigger>
      <SelectContent alignItemWithTrigger={false}>
        <SelectItem value="newest">Newest first</SelectItem>
        <SelectItem value="oldest">Oldest first</SelectItem>
        <SelectItem value="popular">Most popular</SelectItem>
      </SelectContent>
    </Select>
  )
}
```

### Right to left

The trigger, list and check follow the reading direction.

```tsx title="components/examples/select/rtl.tsx"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const cities = [
  { value: "cairo", label: "القاهرة" },
  { value: "riyadh", label: "الرياض" },
  { value: "dubai", label: "دبي" },
]

export function SelectRtl() {
  return (
    <div dir="rtl">
      <Select items={cities} defaultValue="riyadh">
        <SelectTrigger aria-label="المدينة" className="w-44">
          <SelectValue />
        </SelectTrigger>
        <SelectContent>
          {cities.map((city) => (
            <SelectItem key={city.value} value={city.value}>
              {city.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Space` `Enter` `↓` `↑` | Opens the list from the trigger. |
| `↓` `↑` | Moves between options. |
| `Home` `End` | Moves to the first or last option. |
| `A–Z` | Jumps to the next option that starts with the typed text. |
| `Enter` `Space` | Picks the highlighted option. |
| `Esc` | Closes the list and returns focus to the trigger. |

## Accessibility

- Label the trigger with `FieldLabel` or `aria-label`.
- On touch, the list opens below the trigger instead of over it, so your finger doesn't land on an option.

## API reference

### Select

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Value \| Value[] \| null` | – |  |
| `defaultValue` | `Value \| Value[] \| null` | – |  |
| `onValueChange` | `(value, details) => void` | – |  |
| `items` | `Record<string, ReactNode> \| { value, label }[]` | – | Lets SelectValue show labels. |
| `multiple` | `boolean` | `false` |  |
| `name` | `string` | – |  |
| `required` | `boolean` | `false` |  |
| `disabled` | `boolean` | `false` |  |
| `readOnly` | `boolean` | `false` |  |
| `open` | `boolean` | – |  |
| `onOpenChange` | `(open, details) => void` | – |  |

### SelectTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<button>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="select-trigger"` | The trigger, with data-size. |
| `data-popup-open` | Present while the list is open. |
| `data-placeholder` | Present while nothing is picked. |

### SelectValue

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `ReactNode` | – |  |
| `children` | `ReactNode \| (value) => ReactNode` | – | Format the shown value. |

### SelectContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `alignItemWithTrigger` | `boolean` | `true` | Open over the trigger with the current option lined up. |
| `side` | `"top" \| "bottom" \| …` | `"bottom"` | When not aligned with the trigger. |
| `align` | `"start" \| "center" \| "end"` | `"start"` |  |
| `sideOffset` | `number` | `6` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="select-content"` | The popup. |
| `data-side="none"` | Present while aligned over the trigger. |

### SelectItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Value` | – |  |
| `disabled` | `boolean` | `false` |  |
| `label` | `string` | – | Text for typeahead. |

| Attribute | Description |
| --- | --- |
| `data-selected` | The picked option. |
| `data-highlighted` | The focused option. |
| `data-disabled` | The option is disabled. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
