ComboboxSingleSelect
A ComboboxSingleSelect is a combination of a text input and a list of options. It allows users to select a value from a predefined list or filter the list by typing.
Code example
import { ComboboxSingleSelect } from '@yleisradio/yds-components-react';
<ComboboxSingleSelect
label="Label"
id="basic-usage"
placeholder="Select an option"
items={[
{ value: 'News' },
{ value: 'Sports' },
{ value: 'Culture', isDisabled: true }
]}
/>
When to use
- You have a list of options that users need to search and select from
- The list is too long for a simple dropdown (typically 10+ items)
- Users need to type to filter options quickly
- You want to provide autocomplete functionality
- You have fewer than 10 options (use Select instead)
- Users need to select multiple items (use a multi-select pattern or Checkbox group)
- The input should accept free-form text without selection (use TextInput)
- You need a searchable list without single-value selection
Content Guidelines
Combobox labels should be clear and descriptive. The placeholder should guide the user on what to type or select.
- Use short, descriptive labels (e.g., "Country", "Language").
- Use placeholder text to indicate that typing is allowed (e.g., "Type to search...").
- Ensure item labels are consistent and easy to read.
Anatomy
- Label – Describes the purpose of the field.
- Input field – Area where users type to filter or view the selected value.
- Dropdown icon – Indicates that there are options to choose from.
- Menu – The list of filtered options.
- Clear button (optional) – Allows clearing the selection.
Key Props
Use the following props to customize the ComboboxSingleSelect component to fit your needs.
items
The source of truth for the list of options. Each item must have a value property.
| Type | Example | Description |
|---|---|---|
ComboboxItem[] | Array of items to display. |
Code example
<ComboboxSingleSelect label="Category" items={[{ value: 'News' }, { value: 'Sports' }]} />
menuItemComponent
Allows customizing the rendering of menu items.
| Type | Example | Description |
|---|---|---|
(props: ComboboxItemProps) => JSX.Element | Render function for each menu item. |
Code example
const CustomItem = ({
item,
index,
highlightedIndex,
selectedItem,
itemProps,
}: ComboboxItemProps) => {
const isHighlighted = highlightedIndex === index;
const isSelected = item.value === selectedItem?.value;
return (
<li
{...itemProps}
style={{
padding: '8px 16px',
cursor: 'pointer',
backgroundColor: isHighlighted ? '#e0f2fe' : 'transparent',
fontWeight: isSelected ? 'bold' : 'normal',
}}
>
{item.value} {isSelected && '✓'}
</li>
);
};
<ComboboxSingleSelect
label="Maakunta"
items={maakunnat}
menuItemComponent={CustomItem}
/>
isDisabled
Non-interactive, dimmed styling.
| Type | Example | Description |
|---|---|---|
boolean | Temporarily prevents user interaction. |
Code example
<ComboboxSingleSelect label="Disabled" isDisabled items={[{ value: 'News' }]} />
errorMessage
| Type | Example | Description |
|---|---|---|
string | Displays a validation error below the field. Sets aria-invalid. |
Code example
<ComboboxSingleSelect
label="Category"
errorMessage="Please select a value"
items={[{ value: 'News' }]}
/>
description
Helper guidance placed below the field.
| Type | Example | Description |
|---|---|---|
string | Provides additional guidance below the field. |
Code example
<ComboboxSingleSelect label="Category" description="Select one..." items={[{ value: 'News' }]} />
Behavior
- Filtering: By default, the component filters items based on a case-insensitive substring match.
- Selection: Clicking an item selects it and closes the menu. Pressing Enter on a highlighted item also selects it.
- Blur: Blurring the input without selecting an item may clear the input or keep the text depending on configuration.
- Keyboard Navigation: Users can navigate the menu using Arrow Up/Down keys and select with Enter.
Accessibility
- The component uses
aria-expanded,aria-haspopup, andaria-activedescendantto communicate state to screen readers. - Keyboard navigation is fully supported.
- Ensure labels are associated with the input.
Implementation examples
Basic Usage (Uncontrolled)
The simplest way to use the combobox is with the items prop. The component handles filtering automatically. This can be useful
when the combobox is part of a form.
Code example
import { ComboboxSingleSelect, type ComboboxItem } from '@yleisradio/yds-components-react';
const maakunnat: ComboboxItem[] = [
{ value: 'Uusimaa' },
{ value: 'Varsinais-Suomi' },
{ value: 'Satakunta' },
{ value: 'Kanta-Häme' },
{ value: 'Pirkanmaa' },
{ value: 'Päijät-Häme' },
{ value: 'Kymenlaakso' },
{ value: 'Etelä-Karjala' },
{ value: 'Etelä-Savo' },
{ value: 'Pohjois-Savo' },
{ value: 'Pohjois-Karjala' },
{ value: 'Keski-Suomi' },
{ value: 'Etelä-Pohjanmaa' },
{ value: 'Pohjanmaa' },
{ value: 'Keski-Pohjanmaa' },
{ value: 'Pohjois-Pohjanmaa' },
{ value: 'Kainuu' },
{ value: 'Lappi' },
{ value: 'Ahvenanmaa' },
];
export const ComboboxUncontrolled = () => {
return <ComboboxSingleSelect label="Maakunta" id="uncontrolled-example" items={maakunnat} />;
};
Controlled Mode (Custom Filtering)
When you provide controlledItems, setControlledItems, and handleInputValueChange, you manage the filtering logic yourself. This is useful for custom filtering (e.g., "starts with") or async loading.
Use this method if the combobox is tied to application state or needs to be controlled from outside the component.
Code example
import { useState, useEffect } from 'react';
import { Button, ComboboxSingleSelect, type ComboboxItem } from '@yleisradio/yds-components-react';
const items: ComboboxItem[] = [
{ value: 'Uusimaa' },
{ value: 'Varsinais-Suomi' },
{ value: 'Satakunta' },
{ value: 'Kanta-Häme' },
{ value: 'Pirkanmaa' },
{ value: 'Päijät-Häme' },
{ value: 'Kymenlaakso' },
{ value: 'Etelä-Karjala' },
{ value: 'Etelä-Savo' },
{ value: 'Pohjois-Savo' },
{ value: 'Pohjois-Karjala' },
{ value: 'Keski-Suomi' },
{ value: 'Etelä-Pohjanmaa' },
{ value: 'Pohjanmaa' },
{ value: 'Keski-Pohjanmaa' },
{ value: 'Pohjois-Pohjanmaa' },
{ value: 'Kainuu' },
{ value: 'Lappi' },
{ value: 'Ahvenanmaa' },
];
export const ComboboxControlled = () => {
const [comboboxValue, setComboboxValue] = useState<string>('Uusimaa');
const [controlledItems, setControlledItems] = useState<ComboboxItem[]>(items);
const [isTyping, setIsTyping] = useState(false);
useEffect(() => {
if (!isTyping && comboboxValue) {
setControlledItems(items);
}
}, [comboboxValue, isTyping]);
const onInputValueChange = (value: string) => {
setIsTyping(true);
const filteredItems = value
? items.filter((item) => item.value.toLowerCase().startsWith(value.toLowerCase()))
: items;
setControlledItems(filteredItems);
if (value === '') {
setComboboxValue('');
setIsTyping(false);
}
if (
filteredItems.length === 1 &&
filteredItems[0].value.toLowerCase() === value.toLowerCase()
) {
setComboboxValue(filteredItems[0].value);
setIsTyping(false);
}
};
const handleRandomize = () => {
setIsTyping(false);
setComboboxValue(items[Math.floor(Math.random() * items.length)]?.value);
};
const handleSelectedItemChange = (item: ComboboxItem | null | undefined) => {
setIsTyping(false);
setComboboxValue(item?.value || '');
};
return (
<div>
<ComboboxSingleSelect
label="Maakunta"
id="controlled-input"
items={items}
controlledItems={controlledItems}
setControlledItems={setControlledItems}
controlledValue={comboboxValue}
handleInputValueChange={onInputValueChange}
handleSelectedItemChange={handleSelectedItemChange}
/>
<div style={{ marginTop: '1rem' }}>Valittu maakunta: {comboboxValue}</div>
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
<Button size="xs" onClick={handleRandomize}>
Arvo
</Button>
<Button
size="xs"
variant="secondary"
onClick={() => {
setControlledItems(items);
setComboboxValue('');
}}
>
Tyhjennä
</Button>
</div>
</div>
);
};
Multi-select Pattern
You can build a multi-select experience on top of the single-select combobox by managing selected values externally and using TagFilter components.
This is an experimental pattern and should be replaced with a proper multi-select Combobox component in the future.
Code example
import { useState } from 'react';
import {
ComboboxSingleSelect,
type ComboboxItem,
TagFilter,
} from '@yleisradio/yds-components-react';
const items: ComboboxItem[] = [
{ value: 'Uusimaa' },
{ value: 'Varsinais-Suomi' },
{ value: 'Satakunta' },
{ value: 'Kanta-Häme' },
{ value: 'Pirkanmaa' },
{ value: 'Päijät-Häme' },
{ value: 'Kymenlaakso' },
{ value: 'Etelä-Karjala' },
{ value: 'Etelä-Savo' },
{ value: 'Pohjois-Savo' },
{ value: 'Pohjois-Karjala' },
{ value: 'Keski-Suomi' },
{ value: 'Etelä-Pohjanmaa' },
{ value: 'Pohjanmaa' },
{ value: 'Keski-Pohjanmaa' },
{ value: 'Pohjois-Pohjanmaa' },
{ value: 'Kainuu' },
{ value: 'Lappi' },
{ value: 'Ahvenanmaa' },
];
export const ComboboxMultiSelect = () => {
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const [searchValue, setSearchValue] = useState('');
const [availableItems, setAvailableItems] = useState<ComboboxItem[]>(items);
const [controlledItems, setControlledItems] = useState<ComboboxItem[]>(items);
const onInputValueChange = (value: string) => {
setSearchValue(value);
const filtered = value
? availableItems.filter((i) => i.value.toLowerCase().includes(value.toLowerCase()))
: availableItems;
setControlledItems(filtered);
};
const onSelectedItemChange = (item?: ComboboxItem | null) => {
if (!item) return;
if (!selectedValues.includes(item.value)) {
setSelectedValues((prev) => [...prev, item.value]);
}
setAvailableItems((prev) => prev.filter((i) => i.value !== item.value));
setControlledItems((prev) => prev.filter((i) => i.value !== item.value));
// Clear input on next tick to win over Downshift's internal update
setTimeout(() => setSearchValue(''), 0);
};
const removeTag = (value: string) => {
setSelectedValues((prev) => prev.filter((v) => v !== value));
const readded = items.find((i) => i.value === value);
if (readded) {
setAvailableItems((prev) => {
const next = [...prev, readded];
next.sort(
(a, b) =>
items.findIndex((i) => i.value === a.value) -
items.findIndex((i) => i.value === b.value)
);
return next;
});
setControlledItems((prev) => {
const next = [...prev, readded];
next.sort(
(a, b) =>
items.findIndex((i) => i.value === a.value) -
items.findIndex((i) => i.value === b.value)
);
return next;
});
}
};
return (
<div>
<ComboboxSingleSelect
label="Maakunnat"
id="multi-select"
placeholder="Hae maakuntaa..."
description="Valitse maakunnat kirjoittamalla ja klikkaamalla."
items={availableItems}
controlledItems={controlledItems}
setControlledItems={setControlledItems}
controlledValue={searchValue}
handleInputValueChange={onInputValueChange}
handleSelectedItemChange={onSelectedItemChange}
aria-describedby="multi-select-selected-values"
/>
<div
style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '8px' }}
id="multi-select-selected-values"
aria-label={
selectedValues.length > 0
? `Valittuna: ${selectedValues.map((val) => val).join(', ')}`
: ''
}
>
{selectedValues.map((val) => (
<TagFilter
key={val}
text={val}
size="md"
aria-label={'Poista ' + val}
onClose={() => removeTag(val)}
/>
))}
</div>
</div>
);
};
API Reference
Props
The ComboboxSingleSelect is based on the TextInput component and Downshift.js. It accepts standard HTML input attributes and the following props:
Basic
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | Yes | — | Label text displayed above the input. |
items | ComboboxItem[] | Yes | — | Array of items to display in the dropdown. Each item must have a value property and can optionally have isDisabled. |
id | string | No | — | HTML id for the input element. |
placeholder | string | No | — | Placeholder text shown when input is empty. |
value | string | No | — | Initial value for uncontrolled usage. |
Appearance
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | No | 'text' | HTML input type. |
menuSize | 'sm' | 'md' | No | 'md' | Size of the menu items. |
menuMaxHeight | string | No | '300px' | Maximum height of the dropdown menu (CSS value). |
success | boolean | No | false | Shows success state styling. |
isDisabled | boolean | No | false | Disables the entire combobox. |
isRequired | boolean | No | false | Marks the field as required. |
Content
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
description | string | No | — | Helper text displayed below the input. |
errorMessage | string | No | — | Error message displayed below input (triggers error state). |
noResultsText | string | No | 'Ei tuloksia' | Text shown when no items match the filter. |
loadingText | string | No | 'Ladataan...' | Text shown during loading state. |
toggleButtonText | string | No | 'Näytä vaihtoehdot' | Aria label for the toggle button (chevron icon). |
clearText | string | No | 'Tyhjennä' | Aria label for the clear button. |
searchIconLabel | string | No | 'Haku' | Aria label for the search icon. |
Loading
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
loading | boolean | No | false | Shows loading spinner and loading text in menu. |
showLoadingIndicator | boolean | No | false | Shows loading indicator (spinner) next to the input. |
Behavior
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
showAllValuesOnOpen | boolean | No | false | When true, shows all items when menu opens (even if input has value). |
clearSelectionOnEmptyInput | boolean | No | true | When true, clears selection when input is emptied. |
initialIsOpen | boolean | No | false | Whether menu should be open on mount. |
autocomplete | string | No | — | HTML autocomplete attribute. |
Controlled Mode
| Prop | Type | Required | Description |
|---|---|---|---|
controlledValue | string | No | Externally controlled input value. Use with handleControlledValueChange or handleSelectedItemChange. |
controlledItems | ComboboxItem[] | No | Externally controlled items array. Use with setControlledItems for custom filtering logic. |
setControlledItems | (items: ComboboxItem[]) => void | No | Setter for controlled items array. Required when using controlledItems. |
Icons
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
showSearchIcon | boolean | No | false | Shows a search icon before the input. |
icon | InputIconProps | No | — | Custom icon to replace the default toggle button (ChevronDown). |
iconBefore | InputIconProps | No | — | Icon displayed before input (overridden by search icon if showSearchIcon is true). |
iconClear | InputIconProps | No | — | Custom clear icon configuration. |
Customization
| Prop | Type | Required | Description |
|---|---|---|---|
menuItemComponent | (props: ComboboxItemProps) => JSX.Element | No | Custom component to render each menu item. Receives ComboboxItemProps including item, index, highlightedIndex, selectedItem, etc. |
Callbacks
| Prop | Type | Description |
|---|---|---|
handleControlledValueChange | (value: string) => void | Callback when selected item changes. Receives the item's value string. Use for simple controlled mode. |
handleSelectedItemChange | (item?: ComboboxItem | null) => void | Callback when selection changes. Receives the full ComboboxItem object or null. |
handleInputValueChange | (value: string) => void | Callback on every input change. When provided, disables built-in filtering - you must manually update controlledItems. |
handleItemToString | (item?: ComboboxItem) => string | Custom function to convert a ComboboxItem to string for display in the input. |
onChange | (e: ChangeEvent<HTMLInputElement>) => void | Standard input onChange handler. |
Advanced
| Prop | Type | Description |
|---|---|---|
labelOptions | FormElementLabelProps | Additional props for the label element. Note: downshift's getLabelProps() is merged into this. |
environment | Environment | Custom environment for downshift (useful for iframes/shadow DOM). |
downshiftProps | Partial<UseComboboxProps> | Passthrough object of Downshift's useCombobox hook advanced configuration props for advanced use cases. |
Type Definitions
// TextInput props (base for ComboboxSingleSelect)
type TextInputDSProps = {
label: string;
labelOptions?: FormElementLabelProps;
id?: string;
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
placeholder?: string;
description?: string;
errorMessage?: string;
autocomplete?: string;
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'search';
success?: boolean;
isRequired?: boolean;
isDisabled?: boolean;
children?: ReactNode;
icon?: InputIconProps;
submitButton?: ReactNode;
iconBefore?: InputIconProps;
iconClear?: InputIconProps;
showLoadingIndicator?: boolean;
};
// ComboboxSingleSelect specific props
type ComboboxSingleSelectDSProps = TextInputDSProps & {
items: ComboboxItem[];
menuSize?: 'sm' | 'md';
menuMaxHeight?: string;
menuItemComponent?: (props: ComboboxItemProps) => JSX.Element;
noResultsText?: string;
toggleButtonText?: string;
clearText?: string;
showSearchIcon?: boolean;
searchIconLabel?: string;
loading?: boolean;
loadingText?: string;
controlledValue?: string;
handleControlledValueChange?: (value: string) => void;
handleInputValueChange?: (value: string) => void;
handleItemToString?: (item?: ComboboxItem) => string;
handleSelectedItemChange?: (item?: ComboboxItem | null) => void;
controlledItems?: ComboboxItem[];
setControlledItems?: (items: ComboboxItem[]) => void;
showAllValuesOnOpen?: boolean;
clearSelectionOnEmptyInput?: boolean;
initialIsOpen?: boolean;
environment?: Environment;
downshiftProps?: Partial<UseComboboxProps>;
};
// Full props type
type ComboboxSingleSelectProps = InputHTMLAttributes<HTMLInputElement> &
ComboboxSingleSelectDSProps;
// Items in `items` array must conform to this structure
type ComboboxItem = {
value: string;
isDisabled?: boolean;
[key: string]: string | number | boolean | undefined;
};
// Custom menu item component receives these props
type ComboboxItemProps = HTMLAttributes<HTMLLIElement> & {
item: ComboboxItem;
index: number;
highlightedIndex?: number;
selectedItem: ComboboxItem | null;
itemProps: HTMLAttributes<HTMLLIElement>;
menuSize: 'sm' | 'md';
children: ReactNode;
};
// Custom icon configurations
type InputIconProps = {
componentFn: React.ComponentType;
ariaLabel?: string;
onClick?: () => void;
props?: Record<string, unknown>;
};
Related Components
- Select: A form element for simpler dropdowns with fewer options.
- DropdownMenu: A dropdown menu for interactive selections.
- TextInput: For free-form text input.
- TagFilter: For displaying selected items in a multi-select pattern.