Select
An accessible single-select dropdown component built on the WAI-ARIA listbox pattern.
Use it when you need a fully keyboard-navigable, screen-reader friendly alternative to a native <select>.
Usage
Basic usage
import { Select } from '@nofinite/nui';
<Select
options={[
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'orange', label: 'Orange' },
]}
/>;
Controlled usage
const [value, setValue] = useState('apple');
<Select
value={value}
onChange={(v) => setValue(v)}
options={[
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
]}
/>;
Uncontrolled usage
<Select
defaultValue="b"
options={[
{ value: 'a', label: 'Apple' },
{ value: 'b', label: 'Banana' },
{ value: 'c', label: 'Cherry' },
]}
/>
Variants
This component does not expose explicit visual variants. Styling is theme-driven via CSS variables and supports light/dark themes.
<div data-theme="dark">
<Select options={[{ value: '1', label: 'Option A' }]} />
</div>
Guidelines:
- Use themes instead of variants for consistent design.
- Avoid creating multiple Select styles with custom CSS overrides.
Sizes
The Select does not expose size props.
- Width is determined by content or container.
- Minimum width is 200px by default.
- Use layout styles for sizing.
<Select style={{ width: 300 }} />
States
Disabled
<Select disabled options={[{ value: 'a', label: 'Apple' }]} />
- Trigger cannot be opened.
- Keyboard and mouse interactions are blocked.
- Proper
aria-disabledis applied.
Disabled options
<Select
options={[
{ value: 'card', label: 'Credit Card' },
{ value: 'cod', label: 'Cash on Delivery', disabled: true },
]}
/>
- Disabled options are skipped during keyboard navigation.
- Cannot be selected via mouse or keyboard.
Native Props / Composition
The Select trigger is rendered as a <button> and forwards relevant attributes.
<Select
id="payment-method"
className="my-select"
aria-label="Select payment method"
options={[{ value: 'upi', label: 'UPI' }]}
/>
Form compatibility
If name is provided, a hidden <input> is rendered for form submissions.
<Select
name="fruit"
options={[
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
]}
/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | { value: string; label: ReactNode; disabled?: boolean }[] | — | Options rendered in the listbox |
value | string | — | Controlled selected value |
defaultValue | string | — | Initial value for uncontrolled usage |
onChange | (value: string) => void | — | Called when selection changes |
placeholder | string | "Select..." | Shown when no value is selected |
disabled | boolean | false | Disables the Select |
name | string | — | Adds hidden input for forms |
id | string | auto-generated | Base id for ARIA relationships |
className | string | "" | Additional wrapper class |
Behavior Notes
- Supports controlled and uncontrolled modes.
- Uses a portal to render the listbox.
- Automatically restores focus to the trigger on close.
- Clicking outside closes the listbox.
- Active option is managed internally (DOM focus remains on listbox).
Keyboard support
- ArrowUp / ArrowDown – move active option
- Home / End – jump to first / last option
- Enter / Space – select active option
- Esc – close listbox
- Typeahead – type letters to jump to matching options
Accessibility
- Implements the WAI-ARIA listbox pattern
- Trigger:
<button aria-haspopup="listbox"> - Listbox:
<div role="listbox"> - Options:
<div role="option"> - Uses
aria-selected,aria-disabled, andaria-labelledby - Fully keyboard navigable
- Screen-reader friendly selection announcements
Example:
<Select
aria-label="Choose a fruit"
options={[{ value: 'a', label: 'Apple' }]}
/>
Layout
- Inline-block component by default
- Listbox is positioned relative to trigger using fixed positioning
- Width matches trigger minimum width
<div style={{ width: 320 }}>
<Select options={[{ value: 'x', label: 'Wide Select' }]} />
</div>
Best Practices
Do
- Use for single selection only.
- Provide clear labels for options.
- Use
namewhen integrating with forms. - Keep option labels short and readable.
Don’t
- Use for multi-select scenarios.
- Disable all options.
- Nest complex interactive elements inside option labels.
- Override ARIA attributes manually.