Skip to main content
In progress

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.

  • News
  • Sports
  • Culture
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

Do
  • 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
Don't
  • 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.

Do
  • 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

  1. Label – Describes the purpose of the field.
  2. Input field – Area where users type to filter or view the selected value.
  3. Dropdown icon – Indicates that there are options to choose from.
  4. Menu – The list of filtered options.
  5. 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.

TypeExampleDescription
ComboboxItem[]
  • News
  • Sports
Array of items to display.
Code example
<ComboboxSingleSelect label="Category" items={[{ value: 'News' }, { value: 'Sports' }]} />

Allows customizing the rendering of menu items.

TypeExampleDescription
(props: ComboboxItemProps) => JSX.Element
  • News
  • Sports
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.

TypeExampleDescription
boolean
  • News
  • Sports
Temporarily prevents user interaction.
Code example
<ComboboxSingleSelect label="Disabled" isDisabled items={[{ value: 'News' }]} />

errorMessage

TypeExampleDescription
string
  • News
  • Sports
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.

TypeExampleDescription
string
  • News
  • Sports

Select one...

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, and aria-activedescendant to 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.

  • Uusimaa
  • Varsinais-Suomi
  • Satakunta
  • Kanta-Häme
  • Pirkanmaa
  • Päijät-Häme
  • Kymenlaakso
  • Etelä-Karjala
  • Etelä-Savo
  • Pohjois-Savo
  • Pohjois-Karjala
  • Keski-Suomi
  • Etelä-Pohjanmaa
  • Pohjanmaa
  • Keski-Pohjanmaa
  • Pohjois-Pohjanmaa
  • Kainuu
  • Lappi
  • Ahvenanmaa
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.

  • Uusimaa
  • Varsinais-Suomi
  • Satakunta
  • Kanta-Häme
  • Pirkanmaa
  • Päijät-Häme
  • Kymenlaakso
  • Etelä-Karjala
  • Etelä-Savo
  • Pohjois-Savo
  • Pohjois-Karjala
  • Keski-Suomi
  • Etelä-Pohjanmaa
  • Pohjanmaa
  • Keski-Pohjanmaa
  • Pohjois-Pohjanmaa
  • Kainuu
  • Lappi
  • Ahvenanmaa
Valittu maakunta: Uusimaa
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.

  • Uusimaa
  • Varsinais-Suomi
  • Satakunta
  • Kanta-Häme
  • Pirkanmaa
  • Päijät-Häme
  • Kymenlaakso
  • Etelä-Karjala
  • Etelä-Savo
  • Pohjois-Savo
  • Pohjois-Karjala
  • Keski-Suomi
  • Etelä-Pohjanmaa
  • Pohjanmaa
  • Keski-Pohjanmaa
  • Pohjois-Pohjanmaa
  • Kainuu
  • Lappi
  • Ahvenanmaa

Valitse maakunnat kirjoittamalla ja klikkaamalla.

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

PropTypeRequiredDefaultDescription
labelstringYesLabel text displayed above the input.
itemsComboboxItem[]YesArray of items to display in the dropdown. Each item must have a value property and can optionally have isDisabled.
idstringNoHTML id for the input element.
placeholderstringNoPlaceholder text shown when input is empty.
valuestringNoInitial value for uncontrolled usage.

Appearance

PropTypeRequiredDefaultDescription
typestringNo'text'HTML input type.
menuSize'sm' | 'md'No'md'Size of the menu items.
menuMaxHeightstringNo'300px'Maximum height of the dropdown menu (CSS value).
successbooleanNofalseShows success state styling.
isDisabledbooleanNofalseDisables the entire combobox.
isRequiredbooleanNofalseMarks the field as required.

Content

PropTypeRequiredDefaultDescription
descriptionstringNoHelper text displayed below the input.
errorMessagestringNoError message displayed below input (triggers error state).
noResultsTextstringNo'Ei tuloksia'Text shown when no items match the filter.
loadingTextstringNo'Ladataan...'Text shown during loading state.
toggleButtonTextstringNo'Näytä vaihtoehdot'Aria label for the toggle button (chevron icon).
clearTextstringNo'Tyhjennä'Aria label for the clear button.
searchIconLabelstringNo'Haku'Aria label for the search icon.

Loading

PropTypeRequiredDefaultDescription
loadingbooleanNofalseShows loading spinner and loading text in menu.
showLoadingIndicatorbooleanNofalseShows loading indicator (spinner) next to the input.

Behavior

PropTypeRequiredDefaultDescription
showAllValuesOnOpenbooleanNofalseWhen true, shows all items when menu opens (even if input has value).
clearSelectionOnEmptyInputbooleanNotrueWhen true, clears selection when input is emptied.
initialIsOpenbooleanNofalseWhether menu should be open on mount.
autocompletestringNoHTML autocomplete attribute.

Controlled Mode

PropTypeRequiredDescription
controlledValuestringNoExternally controlled input value. Use with handleControlledValueChange or handleSelectedItemChange.
controlledItemsComboboxItem[]NoExternally controlled items array. Use with setControlledItems for custom filtering logic.
setControlledItems(items: ComboboxItem[]) => voidNoSetter for controlled items array. Required when using controlledItems.

Icons

PropTypeRequiredDefaultDescription
showSearchIconbooleanNofalseShows a search icon before the input.
iconInputIconPropsNoCustom icon to replace the default toggle button (ChevronDown).
iconBeforeInputIconPropsNoIcon displayed before input (overridden by search icon if showSearchIcon is true).
iconClearInputIconPropsNoCustom clear icon configuration.

Customization

PropTypeRequiredDescription
menuItemComponent(props: ComboboxItemProps) => JSX.ElementNoCustom component to render each menu item. Receives ComboboxItemProps including item, index, highlightedIndex, selectedItem, etc.

Callbacks

PropTypeDescription
handleControlledValueChange(value: string) => voidCallback when selected item changes. Receives the item's value string. Use for simple controlled mode.
handleSelectedItemChange(item?: ComboboxItem | null) => voidCallback when selection changes. Receives the full ComboboxItem object or null.
handleInputValueChange(value: string) => voidCallback on every input change. When provided, disables built-in filtering - you must manually update controlledItems.
handleItemToString(item?: ComboboxItem) => stringCustom function to convert a ComboboxItem to string for display in the input.
onChange(e: ChangeEvent<HTMLInputElement>) => voidStandard input onChange handler.

Advanced

PropTypeDescription
labelOptionsFormElementLabelPropsAdditional props for the label element. Note: downshift's getLabelProps() is merged into this.
environmentEnvironmentCustom environment for downshift (useful for iframes/shadow DOM).
downshiftPropsPartial<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>;
};
  • 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.