Combobox
A fully WCAG-compliant combobox component for selecting options with support for icons, keyboard navigation, and controlled/uncontrolled usage.
Usage
Basic usage
import { Combobox } from '@nofinite/nui';
<Combobox
options={[
{ label: 'React', value: 'react' },
{ label: 'Next.js', value: 'next.js' },
{ label: 'TypeScript', value: 'ts' },
]}
/>;
With controlled value and change handler
const [value, setValue] = React.useState('');
<Combobox
value={value}
onChange={setValue}
options={[{ label: 'Apple', value: 'apple' }]}
/>;
With custom filtering
<Combobox
options={options}
filter={(input, opt) =>
opt.label.toLowerCase().startsWith(input.toLowerCase())
}
/>
With custom option rendering
<Combobox
options={options}
renderOption={(opt, active) => (
<div style={active ? { fontWeight: 700 } : {}}>{opt.label}</div>
)}
/>
With icons
import { FaSearch } from 'react-icons/fa';
<Combobox
options={options}
leftIcon={<FaSearch />}
placeholder="Search tech..."
/>;
Variants
This component does not have built-in visual variants.
Styling can be customized via className or inline styles.
Sizes
The component size is defined via CSS and can be adjusted using custom styles:
<Combobox style={{ width: '300px' }} />
States
<Combobox disabled />
<Combobox options={[]} emptyMessage="Nothing found" />
-
disabledPrevents user interaction and visually disables the input. -
emptyMessageDisplayed when no filtered options are available.
Native Props / Composition
Supports forwarding native HTML input attributes:
<Combobox
aria-label="Choose a framework"
onFocus={() => console.log('focused')}
className="my-combobox"
/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | ComboboxOption[] | — | List of selectable options |
value | string | — | Controlled selected value |
defaultValue | string | — | Initial value for uncontrolled usage |
onChange | (value: string) => void | — | Callback when value changes |
placeholder | string | "Select..." | Placeholder text for input |
disabled | boolean | false | Disable user interaction |
className | string | "" | Custom CSS class |
emptyMessage | string | "No results found" | Message when no options match filter |
filter | (input: string, option: ComboboxOption) => boolean | default filter | Custom filter function for options |
leftIcon | React.ReactNode | — | Icon displayed inside input on the left |
rightIcon | React.ReactNode | — | Icon displayed inside input on the right |
renderOption | (option: ComboboxOption, active: boolean) => ReactNode | — | Custom render function for options |
renderOptionIcon | (option: ComboboxOption) => ReactNode | — | Custom icon renderer for listbox options |
Behavior Notes
-
Controlled vs uncontrolled:
- If
valueis provided, the component behaves as controlled. - Otherwise, it manages its own internal state.
- If
-
Keyboard interaction:
ArrowDown/ArrowUpto navigate optionsEnterto select an optionEscapeto close the dropdown
-
Clicking outside closes the dropdown automatically.
-
Options are filtered using the
filterprop or a default label match.
Accessibility
-
Input:
- Uses
role="combobox" - Reflects open state via
aria-expanded
- Uses
-
Listbox:
- Uses
role="listbox"
- Uses
-
Options:
- Use
role="option" - Reflect selection via
aria-selected
- Use
-
Fully keyboard accessible:
- Tab navigation
- Arrow keys for traversal
- Enter and Escape for interaction
Layout
- Renders as a block-level element (
width: 100%) by default. - Input wrapper supports left and right icons with automatic padding.
- Listbox is positioned below the input with a scrollable max height.
<Combobox style={{ width: '100%' }} />
Best Practices
Do
- Use when a searchable or filterable dropdown is required.
- Provide meaningful
labelandvaluepairs. - Use icons sparingly to improve visual clarity.
Don’t
- Render large datasets without filtering.
- Omit
onChangewhen using controlled mode. - Use without accessible labels or placeholders.