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
npx @smicolon/fasla-ui@latest add selectRun init once per project before adding components. See Installation.
Preview
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.
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.
Shown as badges.
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.
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, thecomboboxrole, and makes the menu alistboxof options. A hidden input submits the value with its form, undername. - 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.
labelnames the trigger, and the helper text is read with it. Without a label, passaria-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 ofbody.
Props
Select
| Prop | Type | Default | Description |
|---|---|---|---|
items | SelectOption[] | — | The options as data: value, label and an optional disabled. |
children | ReactNode | — | SelectItem rows, in place of items. |
placeholder | ReactNode | — | 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. |
multiple | boolean | false | Lets the reader choose more than one option. |
display | "badges" | "text" | "badges" | With multiple, shows the choices as badges or as one line of text. |
value | string | string[] | null | — | The chosen value, or values with multiple. Use it with onValueChange. |
defaultValue | string | string[] | null | — | The value chosen on first render. |
onValueChange | (value) => void | — | Called with the new value, also when a badge is removed. |
label | ReactNode | — | The field's label, linked to the trigger for you. |
aria-label | string | — | The trigger's accessible name when there is no visible label. |
aria-labelledby | string | — | The id of an element that names the trigger, in place of label. |
labelEnd | ReactNode | — | A short note at the end of the label row, such as Optional. |
description | ReactNode | — | Helper text under the field. A screen reader reads it with the field. |
descriptionEnd | ReactNode | — | A note at the end of the helper row, such as a count. |
startIcon | ReactNode | — | An icon at the start of the trigger. |
disabled | boolean | false | Disables the field and dims the whole group. |
readOnly | boolean | false | Shows the value but stops it changing. |
required | boolean | false | The form submits only once a value is chosen. |
name | string | — | The name the value is submitted under. |
form | string | — | The id of the form the value is submitted with, when the field sits outside it. |
id | string | — | The trigger's id, for a label element of your own. |
open | boolean | — | Opens or closes the menu, when you control it. |
defaultOpen | boolean | false | Opens the menu on first render. |
onOpenChange | (open) => void | — | Called when the menu opens or closes. |
modal | boolean | true | Blocks 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. |
container | HTMLElement | — | Where the menu renders. Default body. |
className | string | — | Extra CSS classes for the field group. |
SelectItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The value this option submits. |
label | string | — | Text for the trigger, the badges and type-ahead, when the row shows more than text. |
disabled | boolean | false | Shows the option but stops it being chosen. |
children | ReactNode | — | What the row shows. |
Usage
Basic select
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
"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
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>
)
}