Region US East (Virginia) US East (Virginia) US West (Oregon) EU Central (Frankfurt) Asia Pacific (Mumbai)
import { Select, SelectItem } from "@/components/ui/select" ;
export default function SelectDemo () {
return (
< Select
className = "w-full max-w-56"
label = "Region"
placeholder = "Select a region"
defaultSelectedKey = "us-east"
>
< SelectItem id = "us-east" >US East (Virginia)</ SelectItem >
< SelectItem id = "us-west" >US West (Oregon)</ SelectItem >
< SelectItem id = "eu-central" >EU Central (Frankfurt)</ SelectItem >
< SelectItem id = "ap-south" >Asia Pacific (Mumbai)</ SelectItem >
</ Select >
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/select
The CLI installs dependencies and any other components this one uses.
import {
Select,
SelectItem,
SelectItemDescription,
SelectItemLabel,
SelectSection,
} from "@/components/ui/select" ;
< Select label = "Role" placeholder = "Select a role" >
< SelectItem id = "admin" >Admin</ SelectItem >
< SelectItem id = "member" >Member</ SelectItem >
< SelectItem id = "viewer" >Viewer</ SelectItem >
</ Select >
Select — one choice from roughly 5–50 known options, where the options don't need to be visible all the time.
Radio Group — fewer than ~5 options that benefit from being visible side by side.
Combo Box — long lists users will search, or when free-form input is allowed.
Menu — a list of actions, not a value.
< Select > { /* field: label, trigger, description, error */ }
< SelectSection title = "…" > { /* optional group with a heading */ }
< SelectItem id = "…" > { /* option */ }
< Icon />
< SelectItemLabel /> { /* shown in the trigger */ }
< SelectItemDescription /> { /* shown only in the list */ }
</ SelectItem >
</ SelectSection >
</ Select >
Part Renders Notes Select<div> + <button> + popoverWraps label, trigger, description, error message and the listbox popover. Trigger <button>Shows the selected item (or placeholder), prefix and a chevron. SelectItemrole="option"An option. Needs a unique id, and textValue if children aren't plain text. SelectItemLabel<span slot="label">Primary text of a rich item; the only part rendered in the trigger. SelectItemDescription<span slot="description">Secondary text; hidden in the trigger. SelectSectionrole="group"Groups options under an optional title. Hidden <select> <select>Rendered for native form submission and browser autofill when name is set.
outline (default), filled for dense or tinted surfaces, and underlined for minimal, inline forms. The same variants apply to every field component.
outline Medium Low Medium High Critical
filled Medium Low Medium High Critical
underlined Medium Low Medium High Critical
import { Select, SelectItem } from "@/components/ui/select" ;
const items = [ "Low" , "Medium" , "High" , "Critical" ]. map (( name ) => ({
id: name. toLowerCase (),
name,
}));
export default function SelectVariants () {
return (
< div className = "flex w-full max-w-56 flex-col gap-5" >
{([ "outline" , "filled" , "underlined" ] as const ). map (( variant ) => (
< Select
key = {variant}
variant = {variant}
label = {variant}
items = {items}
defaultSelectedKey = "medium"
className = "capitalize"
>
{( item ) => < SelectItem >{item.name}</ SelectItem >}
</ Select >
))}
</ div >
);
}
import { Select, SelectItem } from "@/components/ui/select" ;
export default function SelectSizes () {
return (
< div className = "flex w-full max-w-56 flex-col gap-4" >
{([ "sm" , "md" , "lg" ] as const ). map (( size ) => (
< Select
key = {size}
size = {size}
aria-label = { `Size ${ size }` }
defaultSelectedKey = "a"
>
< SelectItem id = "a" >Size {size}</ SelectItem >
< SelectItem id = "b" >Another option</ SelectItem >
</ Select >
))}
</ div >
);
}
Pass items and a render function instead of static children. Each item needs an id (or key) property; React Aria only re-renders items that changed.
Framework Pick a framework Next.js React Router Vite TanStack Start Astro
import { Select, SelectItem } from "@/components/ui/select" ;
const frameworks = [
{ id: "next" , name: "Next.js" },
{ id: "remix" , name: "React Router" },
{ id: "vite" , name: "Vite" },
{ id: "tanstack" , name: "TanStack Start" },
{ id: "astro" , name: "Astro" },
];
export default function SelectDynamic () {
return (
< Select
className = "w-full max-w-56"
label = "Framework"
placeholder = "Pick a framework"
items = {frameworks}
>
{( item ) => < SelectItem >{item.name}</ SelectItem >}
</ Select >
);
}
Compose icons with SelectItemLabel and SelectItemDescription. The trigger shows only the label, so the closed select stays compact. Set textValue for typeahead.
Role Admin Manage members and settings. Owner Admin Member
import { CrownIcon, ShieldIcon, UserIcon } from "lucide-react" ;
import {
Select,
SelectItem,
SelectItemDescription,
SelectItemLabel,
} from "@/components/ui/select" ;
const roles = [
{
id: "owner" ,
name: "Owner" ,
description: "Full access, including billing." ,
icon: CrownIcon,
},
{
id: "admin" ,
name: "Admin" ,
description: "Manage members and settings." ,
icon: ShieldIcon,
},
{
id: "member" ,
name: "Member" ,
description: "Access projects they're added to." ,
icon: UserIcon,
},
];
export default function SelectRichItems () {
return (
< Select
className = "w-full max-w-64"
label = "Role"
items = {roles}
defaultSelectedKey = "admin"
>
{( role ) => (
< SelectItem textValue = {role.name} className = "items-start" >
< role.icon className = "mt-0.5" />
< span className = "flex flex-col gap-0.5" >
< SelectItemLabel >{role.name}</ SelectItemLabel >
< SelectItemDescription >{role.description}</ SelectItemDescription >
</ span >
</ SelectItem >
)}
</ Select >
);
}
Any element works as leading content. Here an Avatar makes people easier to scan.
Assignee JL Jackson Lee Olivia Martin Jackson Lee Isabella Nguyen William Kim
import { Avatar } from "@/components/ui/avatar" ;
import { Select, SelectItem } from "@/components/ui/select" ;
const people = [
{ id: "olivia" , name: "Olivia Martin" , email: "olivia@acme.dev" },
{ id: "jackson" , name: "Jackson Lee" , email: "jackson@acme.dev" },
{ id: "isabella" , name: "Isabella Nguyen" , email: "bella@acme.dev" },
{ id: "william" , name: "William Kim" , email: "will@acme.dev" },
];
const initials = ( name : string ) =>
name
. split ( " " )
. map (( n ) => n[ 0 ])
. join ( "" );
export default function SelectAvatars () {
return (
< Select
className = "w-full max-w-64"
label = "Assignee"
placeholder = "Unassigned"
items = {people}
defaultSelectedKey = "jackson"
>
{( p ) => (
< SelectItem textValue = {p.name}>
< Avatar size = "xs" colorful alt = {p.name} fallback = { initials (p.name)} />
{p.name}
</ SelectItem >
)}
</ Select >
);
}
SelectSection groups options under a heading. prefix adds an icon to the trigger.
Timezone Select a timezone Eastern Time Central Time Pacific Time Greenwich Mean Time Central European Time India Standard Time Nepal Time Japan Standard Time
import { GlobeIcon } from "lucide-react" ;
import { Select, SelectItem, SelectSection } from "@/components/ui/select" ;
export default function SelectSections () {
return (
< Select
className = "w-full max-w-64"
label = "Timezone"
placeholder = "Select a timezone"
prefix = {< GlobeIcon />}
>
< SelectSection title = "North America" >
< SelectItem id = "est" >Eastern Time</ SelectItem >
< SelectItem id = "cst" >Central Time</ SelectItem >
< SelectItem id = "pst" >Pacific Time</ SelectItem >
</ SelectSection >
< SelectSection title = "Europe" >
< SelectItem id = "gmt" >Greenwich Mean Time</ SelectItem >
< SelectItem id = "cet" >Central European Time</ SelectItem >
</ SelectSection >
< SelectSection title = "Asia" >
< SelectItem id = "ist" >India Standard Time</ SelectItem >
< SelectItem id = "npt" >Nepal Time</ SelectItem >
< SelectItem id = "jst" >Japan Standard Time</ SelectItem >
</ SelectSection >
</ Select >
);
}
The list scrolls within the available viewport height. With the trigger focused, type a few letters to jump to the first matching option.
Country Select a country Focus the trigger and start typing to jump. Argentina Australia Austria Belgium Brazil Canada Chile China Colombia Denmark Egypt Finland France Germany Greece India Indonesia Ireland Israel Italy Japan Kenya Mexico Nepal Netherlands New Zealand Nigeria Norway Pakistan Peru Philippines Poland Portugal Saudi Arabia Singapore South Africa South Korea Spain Sweden Switzerland Thailand Türkiye United Arab Emirates United Kingdom United States Vietnam
import { Select, SelectItem } from "@/components/ui/select" ;
const codes = [
"AR" ,
"AU" ,
"AT" ,
"BE" ,
"BR" ,
"CA" ,
"CL" ,
"CN" ,
"CO" ,
"DK" ,
"EG" ,
"FI" ,
"FR" ,
"DE" ,
"GR" ,
"IN" ,
"ID" ,
"IE" ,
"IL" ,
"IT" ,
"JP" ,
"KE" ,
"MX" ,
"NP" ,
"NL" ,
"NZ" ,
"NG" ,
"NO" ,
"PK" ,
"PE" ,
"PH" ,
"PL" ,
"PT" ,
"SA" ,
"SG" ,
"ZA" ,
"KR" ,
"ES" ,
"SE" ,
"CH" ,
"TH" ,
"TR" ,
"AE" ,
"GB" ,
"US" ,
"VN" ,
];
const names = new Intl. DisplayNames ([ "en" ], { type: "region" });
const countries = codes
. map (( id ) => ({ id, name: names. of (id) ?? id }))
. sort (( a , b ) => a.name. localeCompare (b.name));
export default function SelectLongList () {
return (
< Select
className = "w-full max-w-64"
label = "Country"
placeholder = "Select a country"
description = "Focus the trigger and start typing to jump."
items = {countries}
>
{( c ) => < SelectItem >{c.name}</ SelectItem >}
</ Select >
);
}
Use selectedKey and onSelectionChange to own the value. Set it to null to clear the selection and show the placeholder again.
Status In progress To do In progress Done
Selected: in-progress
Clear
import { useState } from "react" ;
import type { Key } from "react-aria-components" ;
import { Button } from "@/components/ui/button" ;
import { Select, SelectItem } from "@/components/ui/select" ;
export default function SelectControlled () {
const [ status , setStatus ] = useState < Key | null >( "in-progress" );
return (
< div className = "flex w-full max-w-56 flex-col gap-3" >
< Select label = "Status" selectedKey = {status} onSelectionChange = {setStatus}>
< SelectItem id = "todo" >To do</ SelectItem >
< SelectItem id = "in-progress" >In progress</ SelectItem >
< SelectItem id = "done" >Done</ SelectItem >
</ Select >
< p className = "text-muted-foreground text-sm" >
Selected: < code className = "text-foreground" >{ String (status)}</ code >
</ p >
< Button
variant = "outline"
size = "sm"
className = "self-start"
onPress = {() => setStatus ( null )}
>
Clear
</ Button >
</ div >
);
}
isDisabled disables the whole field; disabledKeys disables specific options while keeping them visible. Use description to explain why.
Region Europe United States Europe
Plan Pro Enterprise requires a sales call. Free Pro Enterprise
import { Select, SelectItem } from "@/components/ui/select" ;
export default function SelectDisabled () {
return (
< div className = "flex w-full max-w-56 flex-col gap-5" >
< Select label = "Region" defaultSelectedKey = "eu" isDisabled >
< SelectItem id = "us" >United States</ SelectItem >
< SelectItem id = "eu" >Europe</ SelectItem >
</ Select >
< Select
label = "Plan"
defaultSelectedKey = "pro"
disabledKeys = {[ "enterprise" ]}
description = "Enterprise requires a sales call."
>
< SelectItem id = "free" >Free</ SelectItem >
< SelectItem id = "pro" >Pro</ SelectItem >
< SelectItem id = "enterprise" >Enterprise</ SelectItem >
</ Select >
</ div >
);
}
isRequired blocks form submission until an option is chosen and shows the browser's message. disabledKeys keeps unavailable options from being selected.
import { Form } from "react-aria-components" ;
import { Button } from "@/components/ui/button" ;
import { Select, SelectItem } from "@/components/ui/select" ;
export default function SelectValidation () {
return (
< Form
className = "flex w-full max-w-56 flex-col gap-4"
onSubmit = {( e ) => e. preventDefault ()}
>
< Select
label = "Plan"
name = "plan"
placeholder = "Choose a plan"
isRequired
disabledKeys = {[ "enterprise" ]}
>
< SelectItem id = "free" >Free</ SelectItem >
< SelectItem id = "pro" >Pro</ SelectItem >
< SelectItem id = "enterprise" >Enterprise (contact sales)</ SelectItem >
</ Select >
< Button type = "submit" className = "self-start" >
Continue
</ Button >
</ Form >
);
}
validate returns an error string (or null). With validationBehavior="aria" on the form, errors show live as the value changes instead of on submit.
import { Form } from "react-aria-components" ;
import { Button } from "@/components/ui/button" ;
import { Select, SelectItem } from "@/components/ui/select" ;
export default function SelectCustomValidation () {
return (
< Form
className = "flex w-full max-w-64 flex-col gap-4"
validationBehavior = "aria"
onSubmit = {( e ) => e. preventDefault ()}
>
< Select
label = "Retention"
defaultSelectedKey = "7"
description = "How long we keep request logs."
validate = {( key ) =>
key === "7" ? "7 days is below your compliance minimum of 30." : null
}
>
< SelectItem id = "7" >7 days</ SelectItem >
< SelectItem id = "30" >30 days</ SelectItem >
< SelectItem id = "90" >90 days</ SelectItem >
< SelectItem id = "365" >1 year</ SelectItem >
</ Select >
< Button type = "submit" className = "self-start" >
Save
</ Button >
</ Form >
);
}
Give the select a name and its value is included in FormData, exactly like a native <select>.
import { useState } from "react" ;
import { Form } from "react-aria-components" ;
import { Button } from "@/components/ui/button" ;
import { Select, SelectItem } from "@/components/ui/select" ;
export default function SelectForm () {
const [ data , setData ] = useState < Record < string , FormDataEntryValue > | null >(
null ,
);
return (
< Form
className = "flex w-full max-w-64 flex-col gap-4"
onSubmit = {( e ) => {
e. preventDefault ();
setData (Object. fromEntries ( new FormData (e.currentTarget)));
}}
>
< Select label = "Size" name = "size" defaultSelectedKey = "m" isRequired >
< SelectItem id = "s" >Small</ SelectItem >
< SelectItem id = "m" >Medium</ SelectItem >
< SelectItem id = "l" >Large</ SelectItem >
</ Select >
< Select
label = "Color"
name = "color"
placeholder = "Choose a color"
isRequired
>
< SelectItem id = "black" >Black</ SelectItem >
< SelectItem id = "sand" >Sand</ SelectItem >
< SelectItem id = "olive" >Olive</ SelectItem >
</ Select >
< Button type = "submit" className = "self-start" >
Add to cart
</ Button >
{data && (
< code className = "rounded-md bg-muted px-2 py-1 text-xs" >
{ JSON . stringify (data)}
</ code >
)}
</ Form >
);
}
Small selects with icon prefixes next to a SearchField . Each has an aria-label since there's no visible label.
Open All statuses Open Closed
Recently updated Recently updated Newest Most comments
import { ArrowDownUpIcon, CircleDashedIcon } from "lucide-react" ;
import { SearchField } from "@/components/ui/search-field" ;
import { Select, SelectItem } from "@/components/ui/select" ;
export default function SelectRecipeFilters () {
return (
< div className = "flex w-full max-w-2xl flex-wrap items-center gap-2 rounded-lg border bg-card p-2" >
< SearchField
aria-label = "Search issues"
size = "sm"
className = "min-w-40 flex-1"
/>
< Select
aria-label = "Status"
size = "sm"
prefix = {< CircleDashedIcon />}
defaultSelectedKey = "open"
className = "w-36"
>
< SelectItem id = "all" >All statuses</ SelectItem >
< SelectItem id = "open" >Open</ SelectItem >
< SelectItem id = "closed" >Closed</ SelectItem >
</ Select >
< Select
aria-label = "Sort by"
size = "sm"
prefix = {< ArrowDownUpIcon />}
defaultSelectedKey = "updated"
className = "w-40"
>
< SelectItem id = "updated" >Recently updated</ SelectItem >
< SelectItem id = "created" >Newest</ SelectItem >
< SelectItem id = "comments" >Most comments</ SelectItem >
</ Select >
</ div >
);
}
A select aligned to the end of each row, labelled by the row's title via aria-labelledby.
Theme
Choose how the app looks.
Language
Used for dates, numbers and the interface.
English English नेपाली Deutsch
Start of week
First day shown in calendars.
import { Select, SelectItem } from "@/components/ui/select" ;
const rows = [
{
id: "theme" ,
title: "Theme" ,
description: "Choose how the app looks." ,
value: "system" ,
options: [
[ "light" , "Light" ],
[ "dark" , "Dark" ],
[ "system" , "System" ],
],
},
{
id: "language" ,
title: "Language" ,
description: "Used for dates, numbers and the interface." ,
value: "en" ,
options: [
[ "en" , "English" ],
[ "ne" , "नेपाली" ],
[ "de" , "Deutsch" ],
],
},
{
id: "week" ,
title: "Start of week" ,
description: "First day shown in calendars." ,
value: "mon" ,
options: [
[ "sun" , "Sunday" ],
[ "mon" , "Monday" ],
],
},
];
export default function SelectRecipeSettings () {
return (
< div className = "w-full max-w-lg divide-y rounded-xl border bg-card" >
{rows. map (( r ) => (
< div key = {r.id} className = "flex items-center justify-between gap-6 p-4" >
< div >
< p id = { `${ r . id }-label` } className = "font-medium text-sm" >
{r.title}
</ p >
< p className = "text-muted-foreground text-xs" >{r.description}</ p >
</ div >
< Select
aria-labelledby = { `${ r . id }-label` }
defaultSelectedKey = {r.value}
className = "w-36 shrink-0"
>
{r.options. map (([ id , label ]) => (
< SelectItem key = {id} id = {id}>
{label}
</ SelectItem >
))}
</ Select >
</ div >
))}
</ div >
);
}
The second select's items depend on the first. Reset the child value when the parent changes.
Geography Europe United States Europe Asia Pacific
Region Frankfurt (eu-central-1) Frankfurt (eu-central-1) Ireland (eu-west-1)
import { useState } from "react" ;
import type { Key } from "react-aria-components" ;
import { Select, SelectItem } from "@/components/ui/select" ;
const regions : Record < string , { id : string ; name : string }[]> = {
us: [
{ id: "us-east-1" , name: "Virginia (us-east-1)" },
{ id: "us-west-2" , name: "Oregon (us-west-2)" },
],
eu: [
{ id: "eu-central-1" , name: "Frankfurt (eu-central-1)" },
{ id: "eu-west-1" , name: "Ireland (eu-west-1)" },
],
ap: [
{ id: "ap-south-1" , name: "Mumbai (ap-south-1)" },
{ id: "ap-northeast-1" , name: "Tokyo (ap-northeast-1)" },
],
};
export default function SelectRecipeDependent () {
const [ geo , setGeo ] = useState < Key | null >( "eu" );
const [ region , setRegion ] = useState < Key | null >( "eu-central-1" );
return (
< div className = "grid w-full max-w-md gap-4 sm:grid-cols-2" >
< Select
label = "Geography"
selectedKey = {geo}
onSelectionChange = {( key ) => {
setGeo (key);
setRegion ( null );
}}
>
< SelectItem id = "us" >United States</ SelectItem >
< SelectItem id = "eu" >Europe</ SelectItem >
< SelectItem id = "ap" >Asia Pacific</ SelectItem >
</ Select >
< Select
label = "Region"
placeholder = "Select a region"
items = {geo ? regions[ String (geo)] : []}
selectedKey = {region}
onSelectionChange = {setRegion}
isDisabled = { ! geo}
>
{( r ) => < SelectItem >{r.name}</ SelectItem >}
</ Select >
</ div >
);
}
The trigger is a button with aria-haspopup="listbox"; the selected value and label are announced together.
Always provide a label: label, aria-label, or aria-labelledby.
When name is set, a visually hidden native <select> is rendered so browser autofill and form submission work.
description and errorMessage are linked to the trigger with aria-describedby.
Typeahead works both on the closed trigger and inside the open list.
Key Action Space / Enter / ↓ / ↑ Opens the list ↓ / ↑ Moves focus between options Home / End Focuses the first / last option Page Up / Page Down Moves focus by a page Enter / Space Selects the focused option and closes Esc Closes the list without changing the value Any character Typeahead to the first matching option
On the Select root (use group-data-*/field: to style children):
Attribute Present when data-openThe list is open data-focused / data-focus-visibleThe trigger has focus / keyboard focus data-disabledisDisabled is truedata-invalidValidation failed data-requiredisRequired is true (adds the * to the label)
On each SelectItem:
Attribute Present when data-focused / data-hoveredThe option has virtual focus / hover data-selectedThe option is the current value data-disabledThe option is in disabledKeys
data-slotElement selectRoot label, description, field-errorField text popover, list-boxOverlay and list list-box-item, list-box-sectionOptions and groups
The popover is at least as wide as the trigger (min-w-(--trigger-width)). Pass a className on Select to size the whole field.
Also accepts every prop of React Aria's Select .
Accept every prop of React Aria's Text . They set slot="label" and slot="description" so the option's accessible name and description are wired up automatically.
Combo Box — searchable select with free-form input.
List Box — the same list, always visible, with multiple selection.
Radio Group — for a handful of visible options.
Text Field — shares the same field variants and sizes.