Native Select
The browser's own select element, styled to match prfct. Best on mobile, in long forms and wherever zero JavaScript matters.
import { Field, FieldLabel } from "@/components/ui/field"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"
export default function NativeSelectDemo() {
return (
<Field className="w-64">
<FieldLabel htmlFor="native-select-demo">Billing cycle</FieldLabel>
<NativeSelect
id="native-select-demo"
defaultValue="yearly"
className="w-full"
>
<NativeSelectOption value="monthly">Monthly</NativeSelectOption>
<NativeSelectOption value="quarterly">Quarterly</NativeSelectOption>
<NativeSelectOption value="yearly">
Yearly · save 20%
</NativeSelectOption>
</NativeSelect>
</Field>
)
}Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/native-select.jsonUsage
import {
NativeSelect,
NativeSelectOptGroup,
NativeSelectOption,
} from "@/components/ui/native-select"<NativeSelect defaultValue="yearly">
<NativeSelectOption value="monthly">Monthly</NativeSelectOption>
<NativeSelectOption value="yearly">Yearly</NativeSelectOption>
</NativeSelect>className styles the wrapper, which is w-fit by default — pass a width such as w-full to size the control. Every other prop goes to the <select> element.
Examples
Sizes
sm, default and lg match the heights of Input and Button, so a filter and its action line up.
import { Button } from "@/components/ui/button"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"
const sizes = ["sm", "default", "lg"] as const
export default function NativeSelectSizes() {
return (
<div className="flex flex-col gap-4">
{sizes.map((size) => (
<div key={size} className="flex items-center gap-2">
<NativeSelect
size={size}
defaultValue="7"
aria-label={`Date range (${size})`}
className="w-48"
>
<NativeSelectOption value="1">Last 24 hours</NativeSelectOption>
<NativeSelectOption value="7">Last 7 days</NativeSelectOption>
<NativeSelectOption value="30">Last 30 days</NativeSelectOption>
</NativeSelect>
<Button size={size} variant="outline">
Export
</Button>
</div>
))}
</div>
)
}Option groups
NativeSelectOptGroup adds non-selectable headings to long lists. Operating systems render them natively — indented on desktop, sectioned in mobile pickers.
Used for scheduled reports and reminders.
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import {
NativeSelect,
NativeSelectOptGroup,
NativeSelectOption,
} from "@/components/ui/native-select"
const zones = {
Americas: [
["America/New_York", "New York (UTC−4)"],
["America/Chicago", "Chicago (UTC−5)"],
["America/Los_Angeles", "Los Angeles (UTC−7)"],
],
Europe: [
["Europe/London", "London (UTC+1)"],
["Europe/Warsaw", "Warsaw (UTC+2)"],
["Europe/Helsinki", "Helsinki (UTC+3)"],
],
"Asia Pacific": [
["Asia/Singapore", "Singapore (UTC+8)"],
["Asia/Tokyo", "Tokyo (UTC+9)"],
["Australia/Sydney", "Sydney (UTC+10)"],
],
}
export default function NativeSelectGroups() {
return (
<Field className="w-72">
<FieldLabel htmlFor="native-select-zone">Time zone</FieldLabel>
<NativeSelect
id="native-select-zone"
defaultValue="Europe/Warsaw"
className="w-full"
>
{Object.entries(zones).map(([region, options]) => (
<NativeSelectOptGroup key={region} label={region}>
{options.map(([value, label]) => (
<NativeSelectOption key={value} value={value}>
{label}
</NativeSelectOption>
))}
</NativeSelectOptGroup>
))}
</NativeSelect>
<FieldDescription>
Used for scheduled reports and reminders.
</FieldDescription>
</Field>
)
}Invalid and disabled
A disabled, empty first option works as a placeholder: combined with required, the browser refuses to submit until a real choice is made. Mark invalid selections with aria-invalid and a FieldError, exactly as you would an input.
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"
export default function NativeSelectStates() {
return (
<FieldGroup className="grid w-full max-w-xl gap-5 sm:grid-cols-2">
<Field data-invalid>
<FieldLabel htmlFor="native-select-invalid">Country</FieldLabel>
<NativeSelect
id="native-select-invalid"
defaultValue=""
required
aria-invalid
aria-describedby="native-select-invalid-error"
className="w-full"
>
<NativeSelectOption value="" disabled>
Select a country
</NativeSelectOption>
<NativeSelectOption value="pl">Poland</NativeSelectOption>
<NativeSelectOption value="de">Germany</NativeSelectOption>
</NativeSelect>
<FieldError id="native-select-invalid-error">
Choose where you pay taxes.
</FieldError>
</Field>
<Field data-disabled>
<FieldLabel htmlFor="native-select-disabled">Currency</FieldLabel>
<NativeSelect
id="native-select-disabled"
defaultValue="eur"
disabled
className="w-full"
>
<NativeSelectOption value="eur">Euro (EUR)</NativeSelectOption>
</NativeSelect>
</Field>
</FieldGroup>
)
}Guidelines
When to use
- Mobile-heavy flows: phones open their platform picker, which is faster and familiar.
- Long forms and server-rendered pages that must work before — or without — JavaScript.
- Simple lists of plain-text options where custom rendering adds nothing.
When not to use
- Options that need icons, descriptions or custom layout — use Select.
- Lists long enough to search — use a Combobox.
- Two to five options people should compare at a glance — use a Radio Group or Toggle Group.
Native or custom?
Choose one per product surface and stay consistent; mixing both in one form makes it feel unfinished.
Accessibility
Native Select is a real <select>, so it inherits the platform's keyboard support, screen reader semantics and form behavior with no scripting.
- Label it with a
FieldLabelandhtmlFor/id, or anaria-labelfor compact filters. - System colors. Options use the
CanvasandCanvasTextsystem colors, so the open list follows the operating system and the page'scolor-schemein dark mode. - Contrast. The border uses the
inputtoken at 3:1 against the page; the chevron is decorative and hidden from assistive technology.
| Key | Behavior |
|---|---|
Tab | Moves focus to the select. |
SpaceEnterAlt↓ | Opens the list (platform-dependent). |
↑↓ | Changes the selection — without opening the list on most desktop browsers. |
A–Z | Jumps to the next option starting with that letter. |
API reference
NativeSelect
Renders a wrapper <div> with a <select> and a chevron icon. Accepts all native select props.
NativeSelectOption
Renders an <option>. Accepts all native option props, including value and disabled.
NativeSelectOptGroup
Renders an <optgroup>. Set its label to the group heading.