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

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

PropTypeDefaultDescription
labelstringVisible label, associated with the select via Radix Label.
errorstringError message. When set, the field is marked invalid and the text announced.
disabledbooleanfalseDisables interaction.
childrenReactNodeThe <option> (and <optgroup>) elements, exactly as for a native <select>.
...restSelectHTMLAttributes (minus 'size')All other native <select> attributes (value, defaultValue, onChange, name, …) pass through.

Web Components@ghds/web-components

PropTypeDefaultDescription
labelstringOptional visible label; also the accessible name of the slotted <select>.
invalidbooleanfalseReflected attribute; paints the danger stroke and sets aria-invalid on the <select>.
disabledbooleanfalseReflected attribute; disables and mutes the control (propagated to the slotted <select>).
(default slot)slotA native <select> (with its <option>s).

React Native@ghds/react-native

PropTypeDefaultDescription
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.
selectedValuestringCurrently selected value.
onValueChange(value: string) => voidFires with the chosen value.
placeholderstringShown as a leading, empty-valued option.
labelstringVisible label rendered above the control.
errorstringError message; marks the field invalid and announces the text.
disabledbooleanfalseDisables the control and applies the disabled palette.
accessibilityLabel / testIDstringAccessible name for the picker, and a test handle.

On React Native, NativeSelect wraps @react-native-picker/picker, an optional peer dependency — install it in your app with pnpm 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.

React @ghds/react

Web Components @ghds/web-components