Skip to content
Component installs need the registry setup
VegaStack Design

Native Select

The platform select, tokenized — the OS picker on mobile, option groups, and the same chrome as every other field.

Status
stable
Since
0.10.0
Accessibility pattern
native select

Last updated

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Native Select before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify native-select

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/native-select

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

Usage

import {
  NativeSelect,
  NativeSelectOptGroup,
  NativeSelectOption,
} from "@/components/ui/native-select";

<NativeSelect>
  <NativeSelectOption value="">Select a fruit</NativeSelectOption>
  <NativeSelectOption value="apple">Apple</NativeSelectOption>
</NativeSelect>;

Anatomy

NativeSelect — data-slot="native-select" | "native-select-icon" | "native-select-wrapper"
NativeSelectOption — data-slot="native-select-option"
NativeSelectOptGroup — data-slot="native-select-optgroup"

Examples

Composition

Options sit directly under NativeSelect, or inside a NativeSelectOptGroup.

NativeSelect
├── NativeSelectOption
└── NativeSelectOptGroup
    ├── NativeSelectOption
    └── NativeSelectOption

Groups

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 destructive border holds in every state, focused or not (FOC-14).

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

PropTypeDefaultDescription
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-hidden and pointer-events-none; the select itself owns the whole box.
  • Focus shows base.css's global background tint on :focus-visible, like every other trigger (FOC-4); the border never changes colour (FOC-14).
  • Give it a label: FieldLabel bound with htmlFor, or an aria-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.
ContractStates tested
Behaviourdefault, disabled, invalid
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, invalid, disabled
Visualdefault, focus, invalid, disabled

Do / Don't

Do
Reach for NativeSelect first on mobile and in dense tables — the platform picker is faster and more familiar.
Don't
Use NativeSelect when an option needs an icon, a description or any markup — that is Select.

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/50 is removed outright, outline-none included, so the global :focus-visible tint paints and the border stays border-input (FOC-14 retired the focus:border-ring/70 tint).
  • FOC-14 — the invalid border is aria-invalid:border-destructive (and its dark counterpart), held in every state, focused or not; the aria-invalid halo rings are removed with the rest.
  • FRM-4 — disabled:pointer-events-none is dropped, so a disabled select stays hoverable and a Tooltip can explain it.
  • DOC-2 — cn is imported from @vegastack/design.
  • COL-24, INT-4 — hover and press come from the shared state-layer utility, a translucent foreground layer (--state-hover, --state-pressed) over whatever fill is there, so the step reads the same on every surface in both themes; selected, open and highlighted fills are bg- state-selected / bg-state-hover. Operator, Regent, 2026-10-31.

On this page