Select

Lets the reader choose one option from a list, or several. It has a label, helper text and a prefix icon, as Input does, shows several choices as badges or as text, and comes in four sizes and two corner shapes.

Installation

bash
npx @smicolon/fasla-ui@latest add select

Run init once per project before adding components. See Installation.

Preview

City

Where we deliver your order.

Sizes

Four sizes: xs is 28px tall, sm 32px, md 36px and lg 40px, the same heights as Input. As on Input, only the height and the gap between the icon and the text change: the padding is 12px and the icons 16px at every size. The text is 14px, or 16px on screens narrower than 768px, so iOS does not zoom in.

Radius

radius="standard" rounds the corners by 8px, and radius="rounded" makes a pill. The focus ring follows the shape. The menu keeps its 8px corners either way.

States

Focus is a real state, not a prop: tab to the field, or open it. The trigger draws the focus ring on keyboard focus and for as long as its menu is open. disabled dims the whole field to half opacity, its label and helper text included.

Enabled
Disabled

Multiple select

multiple lets the reader choose several options. The menu stays open while they choose, and a check marks each chosen option. The trigger shows the choices as badges, or as one line of text with display="text". Badges wrap onto more rows, and the field grows to hold them. Press a badge's × to remove it.

Delivery cities

Shown as badges.

Delivery cities

Shown as one line of text.

Labels and icons

The label, the note at the end of the label row, the helper text and the prefix icon work as they do on Input, so the two line up side by side in a form. Put the icon in startIcon; the field sizes it to match.

Optional

As it appears on your ID.

Pickup branch

Optional

We hold your order for 3 days.

6 branches

The menu

The menu opens under the trigger, 4px below it and exactly as wide. Pointing at an option, or moving to it with the arrow keys, highlights it. A check at the end of the row marks the chosen option. Its space is always kept, so the text never moves. On a right-to-left page the check and the chevron swap ends.

Long options

An option longer than the menu is wide wraps onto more lines inside it. In the trigger, the chosen option stays on one line and ends in an ellipsis.

Accessibility

  • Base UI gives the trigger, a button, the combobox role, and makes the menu a listbox of options. A hidden input submits the value with its form, under name.
  • Enter, Space and the arrow keys open the menu. The arrow keys, Home and End move through the options, and typing a letter jumps to a matching one. Enter chooses, and Escape closes the menu and returns focus to the trigger.
  • label names the trigger, and the helper text is read with it. Without a label, pass aria-label. Pressing the label moves focus to the trigger without opening the menu.
  • A badge's × is for the pointer, and screen readers skip it. From the keyboard, open the menu and choose the option again to remove it.
  • Direction is inherited from dir, never a prop. The menu takes the page's direction, language and font from the trigger, though it renders at the end of body.

Props

Select

PropTypeDefaultDescription
itemsSelectOption[]—The options as data: value, label and an optional disabled.
childrenReactNode—SelectItem rows, in place of items.
placeholderReactNode—Text shown while nothing is chosen.
size"xs" | "sm" | "md" | "lg""md"Field height: 28, 32, 36 or 40px.
radius"standard" | "rounded""standard"Corner shape: 8px corners, or fully rounded ends.
multiplebooleanfalseLets the reader choose more than one option.
display"badges" | "text""badges"With multiple, shows the choices as badges or as one line of text.
valuestring | string[] | null—The chosen value, or values with multiple. Use it with onValueChange.
defaultValuestring | string[] | null—The value chosen on first render.
onValueChange(value) => void—Called with the new value, also when a badge is removed.
labelReactNode—The field's label, linked to the trigger for you.
aria-labelstring—The trigger's accessible name when there is no visible label.
aria-labelledbystring—The id of an element that names the trigger, in place of label.
labelEndReactNode—A short note at the end of the label row, such as Optional.
descriptionReactNode—Helper text under the field. A screen reader reads it with the field.
descriptionEndReactNode—A note at the end of the helper row, such as a count.
startIconReactNode—An icon at the start of the trigger.
disabledbooleanfalseDisables the field and dims the whole group.
readOnlybooleanfalseShows the value but stops it changing.
requiredbooleanfalseThe form submits only once a value is chosen.
namestring—The name the value is submitted under.
formstring—The id of the form the value is submitted with, when the field sits outside it.
idstring—The trigger's id, for a label element of your own.
openboolean—Opens or closes the menu, when you control it.
defaultOpenbooleanfalseOpens the menu on first render.
onOpenChange(open) => void—Called when the menu opens or closes.
modalbooleantrueBlocks page scroll and presses outside while the menu is open.
collisionAvoidance{ side, align, fallbackAxisSide }—How the menu avoids the window edge. By default it flips above the trigger when there is no room below; { side: "none" } keeps it below.
containerHTMLElement—Where the menu renders. Default body.
classNamestring—Extra CSS classes for the field group.

SelectItem

PropTypeDefaultDescription
valuestring—The value this option submits.
labelstring—Text for the trigger, the badges and type-ahead, when the row shows more than text.
disabledbooleanfalseShows the option but stops it being chosen.
childrenReactNode—What the row shows.

Usage

Basic select

tsx
import { MapPin } from "lucide-react"
import { Select } from "@/components/ui/select"

const cities = [
  { value: "riyadh", label: "Riyadh" },
  { value: "jeddah", label: "Jeddah" },
  { value: "dammam", label: "Dammam" },
]

export function CityField() {
  return (
    <Select
      name="city"
      label="City"
      placeholder="Select a city"
      startIcon={<MapPin />}
      items={cities}
    />
  )
}

Multiple select, controlled

tsx
"use client"

import * as React from "react"
import { Select } from "@/components/ui/select"

const cities = [
  { value: "riyadh", label: "Riyadh" },
  { value: "jeddah", label: "Jeddah" },
  { value: "dammam", label: "Dammam" },
]

export function DeliveryCities() {
  const [value, setValue] = React.useState<string[]>(["riyadh"])
  return (
    <Select
      multiple
      label="Delivery cities"
      placeholder="Select cities"
      items={cities}
      value={value}
      onValueChange={setValue}
    />
  )
}

With SelectItem rows

tsx
import { Select, SelectItem } from "@/components/ui/select"

export function BranchField() {
  return (
    <Select label="Pickup branch" placeholder="Select a branch">
      <SelectItem value="olaya">Al Olaya branch</SelectItem>
      <SelectItem value="malqa">Al Malqa branch</SelectItem>
      <SelectItem value="nakheel" disabled>
        Al Nakheel branch
      </SelectItem>
    </Select>
  )
}