---
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const items = [
{ label: "Select a fruit", value: null },
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Blueberry", value: "blueberry" },
{ label: "Grapes", value: "grapes" },
{ label: "Pineapple", value: "pineapple" },
]
---
<Select placeholder="Select a fruit" class="w-full max-w-48">
<SelectTrigger class="w-full max-w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
{items.map((item) => (
<SelectItem value={item.value ?? ""}>{item.label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>Installation
npx shadcn@latest add @fulldev/select
Usage
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
<Select placeholder="Theme">
<SelectTrigger class="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
SelectValue shows the label of the selected item, or the placeholder of
Select when nothing is selected. Pass name to submit the value with a form,
and required for native validation. The Select root is an inline-flex w-fit wrapper, so give it the trigger’s width classes when the trigger uses
w-full.
Composition
Select
├── SelectTrigger
│ └── SelectValue
└── SelectContent
├── SelectGroup
│ ├── SelectLabel
│ ├── SelectItem
│ └── SelectItem
├── SelectSeparator
└── SelectGroup
├── SelectLabel
├── SelectItem
└── SelectItem
Align Item With Trigger
Use position on Select or SelectContent. With "item-aligned" (default),
the popup positions so the selected item appears over the trigger. With
"popper", it opens below the trigger and takes side, align,
sideOffset, and alignOffset. Data Slot reads the position when it binds, so
the example rebinds the select when the switch changes.
Toggle to align the item with the trigger.
---
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
const items = [
{ label: "Select a fruit", value: null },
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Blueberry", value: "blueberry" },
{ label: "Grapes", value: "grapes" },
{ label: "Pineapple", value: "pineapple" },
]
---
<FieldGroup class="w-full max-w-xs" data-select-align-item>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel for="align-item">Align Item</FieldLabel>
<FieldDescription>
Toggle to align the item with the trigger.
</FieldDescription>
</FieldContent>
<Switch id="align-item" defaultChecked />
</Field>
<Field>
<Select placeholder="Select a fruit" defaultValue="banana">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{items.map((item) => (
<SelectItem value={item.value ?? ""}>{item.label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
</FieldGroup>
<script>
import { createSelect } from "@data-slot/select"
// Data Slot reads the position when it binds, so rebind on a toggle.
const initialize = () => {
for (const root of document.querySelectorAll("[data-select-align-item]")) {
if (root.hasAttribute("data-bound")) continue
root.setAttribute("data-bound", "")
const select = root.querySelector('[data-slot="select"]')
root
.querySelector('[data-slot="switch"]')
?.addEventListener("switch:change", (event) => {
if (!select) return
const { checked } = (event as CustomEvent<{ checked: boolean }>)
.detail
const current = createSelect(select)
const value = current.value ?? undefined
current.destroy()
createSelect(select, {
defaultValue: value,
position: checked ? "item-aligned" : "popper",
})
})
}
}
initialize()
document.addEventListener("astro:page-load", initialize)
</script>Groups
Use SelectGroup, SelectLabel, and SelectSeparator to organize items.
---
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const fruits = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Blueberry", value: "blueberry" },
]
const vegetables = [
{ label: "Carrot", value: "carrot" },
{ label: "Broccoli", value: "broccoli" },
{ label: "Spinach", value: "spinach" },
]
---
<Select placeholder="Select a fruit" class="w-full max-w-48">
<SelectTrigger class="w-full max-w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Fruits</SelectLabel>
{fruits.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Vegetables</SelectLabel>
{vegetables.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>Scrollable
A select with many items that scrolls.
---
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const northAmerica = [
{ label: "Eastern Standard Time", value: "est" },
{ label: "Central Standard Time", value: "cst" },
{ label: "Mountain Standard Time", value: "mst" },
{ label: "Pacific Standard Time", value: "pst" },
{ label: "Alaska Standard Time", value: "akst" },
{ label: "Hawaii Standard Time", value: "hst" },
]
const europeAfrica = [
{ label: "Greenwich Mean Time", value: "gmt" },
{ label: "Central European Time", value: "cet" },
{ label: "Eastern European Time", value: "eet" },
{ label: "Western European Summer Time", value: "west" },
{ label: "Central Africa Time", value: "cat" },
{ label: "East Africa Time", value: "eat" },
]
const asia = [
{ label: "Moscow Time", value: "msk" },
{ label: "India Standard Time", value: "ist" },
{ label: "China Standard Time", value: "cst_china" },
{ label: "Japan Standard Time", value: "jst" },
{ label: "Korea Standard Time", value: "kst" },
{ label: "Indonesia Central Standard Time", value: "ist_indonesia" },
]
const australiaPacific = [
{ label: "Australian Western Standard Time", value: "awst" },
{ label: "Australian Central Standard Time", value: "acst" },
{ label: "Australian Eastern Standard Time", value: "aest" },
{ label: "New Zealand Standard Time", value: "nzst" },
{ label: "Fiji Time", value: "fjt" },
]
const southAmerica = [
{ label: "Argentina Time", value: "art" },
{ label: "Bolivia Time", value: "bot" },
{ label: "Brasilia Time", value: "brt" },
{ label: "Chile Standard Time", value: "clt" },
]
---
<Select placeholder="Select a timezone" class="w-full max-w-64">
<SelectTrigger class="w-full max-w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>North America</SelectLabel>
{northAmerica.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>Europe & Africa</SelectLabel>
{europeAfrica.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>Asia</SelectLabel>
{asia.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>Australia & Pacific</SelectLabel>
{australiaPacific.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
<SelectGroup>
<SelectLabel>South America</SelectLabel>
{southAmerica.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>Disabled
Pass disabled to Select or to a SelectItem.
---
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const items = [
{ label: "Select a fruit", value: null },
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Blueberry", value: "blueberry" },
{ label: "Grapes", value: "grapes", disabled: true },
{ label: "Pineapple", value: "pineapple" },
]
---
<Select placeholder="Select a fruit" disabled class="w-full max-w-48">
<SelectTrigger class="w-full max-w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{items.map((item) => (
<SelectItem value={item.value ?? ""} disabled={item.disabled}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>Invalid
Add the data-invalid attribute to the Field component and the
aria-invalid attribute to the SelectTrigger component to show an error
state.
---
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const items = [
{ label: "Select a fruit", value: null },
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Blueberry", value: "blueberry" },
]
---
<Field data-invalid class="w-full max-w-48">
<FieldLabel>Fruit</FieldLabel>
<Select placeholder="Select a fruit">
<SelectTrigger aria-invalid="true">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{items.map((item) => (
<SelectItem value={item.value ?? ""}>{item.label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<FieldError>Please select a fruit.</FieldError>
</Field>RTL
---
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
const translations = {
en: {
dir: "ltr",
values: {
selectFruit: "Select a fruit",
fruits: "Fruits",
apple: "Apple",
banana: "Banana",
blueberry: "Blueberry",
grapes: "Grapes",
pineapple: "Pineapple",
vegetables: "Vegetables",
carrot: "Carrot",
broccoli: "Broccoli",
spinach: "Spinach",
},
},
ar: {
dir: "rtl",
values: {
selectFruit: "اختر فاكهة",
fruits: "الفواكه",
apple: "تفاح",
banana: "موز",
blueberry: "توت أزرق",
grapes: "عنب",
pineapple: "أناناس",
vegetables: "الخضروات",
carrot: "جزر",
broccoli: "بروكلي",
spinach: "سبانخ",
},
},
he: {
dir: "rtl",
values: {
selectFruit: "בחר פרי",
fruits: "פירות",
apple: "תפוח",
banana: "בננה",
blueberry: "אוכמניה",
grapes: "ענבים",
pineapple: "אננס",
vegetables: "ירקות",
carrot: "גזר",
broccoli: "ברוקולי",
spinach: "תרד",
},
},
}
const language = "ar"
const { dir, values: t } = translations[language]
const fruits = [
{ label: t.apple, value: "apple" },
{ label: t.banana, value: "banana" },
{ label: t.blueberry, value: "blueberry" },
{ label: t.grapes, value: "grapes" },
{ label: t.pineapple, value: "pineapple" },
]
const vegetables = [
{ label: t.carrot, value: "carrot" },
{ label: t.broccoli, value: "broccoli" },
{ label: t.spinach, value: "spinach" },
]
---
<Select placeholder={t.selectFruit}>
<SelectTrigger class="w-32" dir={dir}>
<SelectValue />
</SelectTrigger>
<SelectContent dir={dir} data-lang={dir === "rtl" ? language : undefined}>
<SelectGroup>
<SelectLabel>{t.fruits}</SelectLabel>
{fruits.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>{t.vegetables}</SelectLabel>
{vegetables.map((item) => (
<SelectItem value={item.value}>{item.label}</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>Content mounting
Data Slot’s recommended "lazy" strategy is the default: closed content is
retained but detached from the document. Fulldev initializes nested Data Slot
components inside that retained content too.
Use mountStrategy="eager" when an integration needs to query closed content
through the document. Both strategies include content in server-rendered HTML;
neither adds a no-JavaScript fallback.
API Reference
Source: src/components/ui/select
Select
| Prop | Type | Default |
|---|---|---|
defaultValue | string | |
placeholder | string | "" |
disabled | boolean | false |
required | boolean | false |
name | string | |
position | "item-aligned" | "popper" | "item-aligned" |
side | "top" | "bottom" | "bottom" |
align | "start" | "center" | "end" | "start" |
sideOffset | number | 4 |
alignOffset | number | 0 |
avoidCollisions | boolean | true |
collisionPadding | number | 8 |
highlightItemOnHover | boolean | true |
lockScroll | boolean | true |
mountStrategy | "lazy" | "eager" | "lazy" |
SelectContent takes the placement props too and overrides the root.
SelectItem takes value, disabled, and label (the text shown in the
trigger when it differs from the item content). SelectTrigger takes size
("default" \| "sm").
The root emits select:change with { value } and select:open-change with
{ open }. Dispatch select:set with { value?, open? } to change it from
client code. See the
Data Slot select docs
for keyboard and controller details.