Select

A hand-drawn single-select dropdown with a fully custom, hand-implemented listbox.

Overview

A Select lets someone choose one option from a list, shown in a floating panel anchored to a button trigger. Unlike every other GHDS form control, it does not wrap a real native <select> — a native dropdown’s browser-drawn UI can’t be restyled to match the hand-drawn aesthetic, so the trigger is a real <button> but the panel and its keyboard model are hand-implemented, following the WAI-ARIA “select-only combobox” pattern.

Anatomy

A sketchy trigger button (selected value or placeholder, plus a chevron that flips when open): clicking or pressing Enter/Space/Arrow keys opens a floating listbox panel positioned below (or above, if there isn’t room) the trigger. Real DOM focus never leaves the trigger — the currently highlighted option is tracked via aria-activedescendant, not by moving focus into the panel.

Variants & States

State Status Notes
Default Implemented
Open Implemented Floating panel, positioned via @floating-ui/dom (React/Web Components) or a measured Modal overlay (React Native).
Selected Implemented
Disabled Implemented Trigger and individual options can each be disabled independently.
Loading Not implemented

Usage

Do

  • Use Select when there are more options than comfortably fit as visible Radio buttons (roughly 5+).
  • Keep option labels short and scannable — the panel does not wrap long text.

Don't

  • Don't expect keyboard Arrow navigation to wrap from the last option back to the first — v1 clamps at the ends, matching a native select's own behavior in most browsers.
  • Don't rely on the panel repositioning correctly if a transformed or clipped ancestor sits between the trigger and the viewport — position: fixed escapes normal overflow, but not a transform/filter/contain ancestor. This is a known v1 limitation, not a bug.

Accessibility

Content

The placeholder names what’s being chosen (“Choose a fruit”), not an instruction (“Please select”). Option labels name the option itself, not an action.

Props API

React@ghds/react

PropTypeDefaultDescription
labelstringVisible label, associated via Radix Label.
valuestringControlled selected value.
defaultValuestringInitial value when uncontrolled.
onValueChange(value: string) => voidFires when an option is selected.
placeholderstringShown when nothing is selected.
disabledbooleanfalseDisables the trigger.
childrenReactNodeSelectOption elements — the only platform using children composition.

Web Components@ghds/web-components

PropTypeDefaultDescription
options{ value, label, disabled? }[]The selectable options — a data array, not slotted children (light DOM has no context mechanism).
valuestring''Current field value, mirrored to the form value.
namestringSubmitted control name.
placeholderstringShown when nothing is selected.
labelstringOptional visible label, also the accessible name.
disabledbooleanfalseDisables the trigger and form participation.

React Native@ghds/react-native

PropTypeDefaultDescription
labelstringVisible, accessible label — required (there is no separate label element on RN).
options{ value, label, disabled? }[]The selectable options — mirrors the Web Components data-array API.
valuestringControlled selected value.
onValueChange(value: string) => voidFires when an option is selected.
placeholderstringShown when nothing is selected.
disabledbooleanfalseDisables the trigger.
testIDstringTest handle for queries.

Code examples

// React — @ghds/react (children composition with SelectOption)
import { Select, SelectOption } from '@ghds/react/select';
import { useState } from 'react';

const [fruit, setFruit] = useState<string | undefined>(undefined);
<Select label="Fruit" placeholder="Choose a fruit" value={fruit} onValueChange={setFruit}>
  <SelectOption value="apple">Apple</SelectOption>
  <SelectOption value="banana">Banana</SelectOption>
  <SelectOption value="cherry" disabled>
    Cherry (out of stock)
  </SelectOption>
</Select>;
<!-- Web Components — @ghds/web-components -->
<!-- `options` is a data array (JSON attribute), not slotted children -->
<script type="module">
  import '@ghds/web-components/select';
</script>
<gh-select
  label="Fruit"
  placeholder="Choose a fruit"
  options='[{"value":"apple","label":"Apple"},{"value":"banana","label":"Banana"}]'
></gh-select>
// React Native — @ghds/react-native (data-array options, like Web Components)
import { Select } from '@ghds/react-native/select';

const FRUIT_OPTIONS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
];
<Select label="Fruit" options={FRUIT_OPTIONS} value={fruit} onValueChange={setFruit} />;

Live Demo

React and Web Components render live below. 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.

React @ghds/react

Web Components @ghds/web-components