Native Select
The platform select, tokenized — the OS picker on mobile, option groups, and the same chrome as every other field.
- Status
- Since
0.10.0- Accessibility pattern
- native select
Last updated
Install
Add Native Select from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/native-selectUsage
import {
NativeSelect,
NativeSelectOptGroup,
NativeSelectOption,
} from "@/components/ui/native-select";
<NativeSelect>
<NativeSelectOption value="">Select a fruit</NativeSelectOption>
<NativeSelectOption value="apple">Apple</NativeSelectOption>
</NativeSelect>;Anatomy
Examples
Composition
Options sit directly under NativeSelect, or inside a NativeSelectOptGroup.
NativeSelect
├── NativeSelectOption
└── NativeSelectOptGroup
├── NativeSelectOption
└── NativeSelectOptionGroups
NativeSelectOptGroup organises options into categories the platform renders itself.
Disabled
Add disabled to the NativeSelect. The wrapper dims with has-[select:disabled].
Invalid
Use aria-invalid on the select and data-invalid on the Field. The tint is scoped to
not-focus: (FOC-5), so a focused select shows its focus cue rather than the error colour.
Native Select vs Select
- Use NativeSelect for native browser behaviour, better performance, or a mobile-optimised picker.
- Use Select for custom item markup, icons, descriptions, animation, or a scrollable grouped popup.
The platform picker — fastest, and the right control on mobile.
A rendered popup — icons, descriptions and animation.
RTL
The chevron is positioned with end-2.5 and the padding with ps-/pe-, so a right-to-left
document mirrors the whole control.
Sizes
size takes sm (28px) and default (32px).
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
size | "default" | "sm" | — |
NativeSelectOption and NativeSelectOptGroup add no props of their own — each accepts everything
the native <option> and <optgroup> accept.
Accessibility
- Renders a native
<select>, so the platform owns the popup, type-ahead, keyboard model and the mobile picker sheet. Nothing here re-implements any of it. - The chevron is
aria-hiddenandpointer-events-none; the select itself owns the whole box. - Focus shows the border tint on
:focus(FOC-3) and the global 2px outline, because this is a button-style trigger rather than a text field (FOC-4). - Give it a label:
FieldLabelbound withhtmlFor, or anaria-label. - Options set
bg-[Canvas] text-[CanvasText]so the popup stays legible under a forced-colours palette and in a dark scope, where some platforms would otherwise paint dark-on-dark.
| Contract | States tested |
|---|---|
| Behaviour | default, disabled, invalid |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, invalid, disabled |
| Visual | default, focus, invalid, disabled |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/native-select.patch. Every hunk:
- FOC-1, FOC-3, FOC-4, FOC-6 —
outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50becomesfocus:border-ring/70: the border tint fires on:focusfor mouse and keyboard alike, and theoutline-noneis dropped so the global:focus-visibleoutline still paints. A native select is a button-style trigger, so it keeps the outline — unlike Input and Textarea, which suppress it. - FOC-5 — the invalid tint becomes
not-focus:aria-invalid:border-destructive(and its dark counterpart), so focus outranks it; thearia-invalidhalo rings are removed with the rest. - FRM-4 —
disabled:pointer-events-noneis dropped, so a disabled select stays hoverable and a Tooltip can explain it. - DOC-2 —
cnis imported from@vegastack/design.