NativeSelect
A hand-drawn box around a real native select, with a chevron and an error state.
Overview
NativeSelect wraps a real native <select> in a hand-drawn box with a chevron. Unlike Select
(a fully custom listbox), it keeps the platform’s native dropdown — which is the right call on touch
devices, for very long option lists, and when you want the OS’s native picker. Pass <option>
children exactly as you would to a bare <select>.
Anatomy
A Radix Label above a sketchy box containing the native <select> (its own border and arrow
removed via appearance: none) and an overlaid chevron icon. When an error is set, the field is
marked invalid and the message is rendered in a role="alert". Every color, padding, radius, and
sketch parameter comes from @ghds/tokens (comp.nativeSelect.*).
Variants & States
No named variants — the field reacts to focus, and disabled / error set its state.
| State | Status | Notes |
|---|---|---|
| Focus | Implemented | Focus switches the outline to comp.nativeSelect.stroke.focus. |
| Error / invalid | Implemented | Setting error paints the danger stroke and announces the message. |
| Disabled | Implemented | — |
| Hover | Not implemented | — |
| Loading | Not implemented | — |
Usage
Do
- Prefer NativeSelect over the custom Select on touch devices and for long option lists.
- Pass a leading placeholder <option value=""> when there is no sensible default choice.
- Set error together with client-side validation so the danger stroke is explained by the message.
Don't
- Don't reach for NativeSelect when you need rich, non-text options or custom option rendering — use Select.
- Don't rely on the danger stroke alone to convey an error — the error text should say what's wrong.
Accessibility
- A Radix
Labelis associated with the<select>viahtmlFor/id, so the control has a real accessible name and the label is clickable. - Error state is exposed with
aria-invalidandaria-describedbypointing at arole="alert"message, so screen readers announce both the invalid state and the reason. - Because the underlying element is a real
<select>, all native keyboard behavior and the OS picker come for free. - The chevron is decorative (
pointer-events: none) and never intercepts interaction. - See the Accessibility Guide for shared conventions.
Content
Keep option labels short and parallel. Use a placeholder <option value=""> (e.g. “Select a
fruit…”) only when no default is appropriate; otherwise pre-select the most common choice.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Visible label, associated with the select via Radix Label. |
error | string | — | Error message. When set, the field is marked invalid and the text announced. |
disabled | boolean | false | Disables interaction. |
children | ReactNode | — | The <option> (and <optgroup>) elements, exactly as for a native <select>. |
...rest | SelectHTMLAttributes (minus 'size') | — | All other native <select> attributes (value, defaultValue, onChange, name, …) pass through. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Optional visible label; also the accessible name of the slotted <select>. |
invalid | boolean | false | Reflected attribute; paints the danger stroke and sets aria-invalid on the <select>. |
disabled | boolean | false | Reflected attribute; disables and mutes the control (propagated to the slotted <select>). |
(default slot) | slot | — | A native <select> (with its <option>s). |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
items | { label; value; enabled? }[] | — | Data-driven options (React Native has no <option> children). enabled: false only dims the text — the native picker has no per-item disabled. |
selectedValue | string | — | Currently selected value. |
onValueChange | (value: string) => void | — | Fires with the chosen value. |
placeholder | string | — | Shown as a leading, empty-valued option. |
label | string | — | Visible label rendered above the control. |
error | string | — | Error message; marks the field invalid and announces the text. |
disabled | boolean | false | Disables the control and applies the disabled palette. |
accessibilityLabel / testID | string | — | Accessible name for the picker, and a test handle. |
On React Native,
NativeSelectwraps@react-native-picker/picker, an optional peer dependency — install it in your app withpnpm add @react-native-picker/picker.
Code examples
// React — @ghds/react
import { NativeSelect } from '@ghds/react/native-select';
// <option> children + onChange + error string
<NativeSelect label="Fruit" error="Please choose a fruit." onChange={(e) => setValue(e.target.value)}>
<option value="">Select a fruit…</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="cherry">Cherry</option>
</NativeSelect>;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/native-select';
</script>
<!-- Slot a real <select>; `invalid` (boolean), not an error string -->
<gh-native-select label="Fruit" invalid>
<select>
<option value="">Select a fruit…</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="cherry">Cherry</option>
</select>
</gh-native-select>
// React Native — @ghds/react-native — requires the picker peer dependency:
// pnpm add @react-native-picker/picker
import { NativeSelect } from '@ghds/react-native/native-select';
// Data-driven via items[] + selectedValue/onValueChange (no <option> children)
<NativeSelect
label="Fruit"
placeholder="Select a fruit…"
selectedValue={value}
onValueChange={setValue}
items={[
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
]}
/>;
Live Demo
React and Web Components render live below, driven by the same tokens — open either to see the
native OS picker, and toggle dark mode in the header to see both respond identically. React exposes
the message as an error string; Web Components expose the boolean invalid state. React Native
can’t run in a browser without additional react-native-web wiring (not set up in this site), so
its usage is shown as a code sample instead — and it is data-driven (an items array) rather
than taking <option> children.