Skip to main content

ComboBox

Button showing the current choice, with a list of options under it. The choice itself stays in your state: the component reports a click and nothing more.

ComboBoxComboBox

Use this when / not when​

  • Use to pick one value from a short, known list: a role, a time zone, a filter, a language.
  • Not for a value the user types or searches. There is no search field here — withSearch is declared and does nothing — so a long list wants Selector or an input of your own.
  • Not for a menu of actions. A menu is DropDown with DropDownItems; this one is about a value.
  • Not on its own in a form: a labelled field is FieldContainer around it.
  • Not for many values at once. It shows one option and a +N badge you maintain yourself.

Import​

import {
ComboBox,
ComboBoxSize,
} from "@onlyoffice/apps-ui-kit/components/combobox";

Also exported from the root barrel @onlyoffice/apps-ui-kit.

Needs ThemeProvider from @onlyoffice/apps-ui-kit/providers/theme for the button and list colours, and TranslationProvider from @onlyoffice/apps-ui-kit/providers/translation because the options are rendered as DropDownItems, which read a translated label for their paid badge.

Stories​

Default​

A fixed-width combo box with a placeholder in the button, as a form shows it before anything is chosen. Pick an option to see it replace the placeholder, and change any other prop live in the Controls panel below.

DefaultDefault

Different Sizes​

The fixed widths side by side, to pick the one that fits the longest label a form expects: base 173px, middle 300px, big 350px, huge 500px, and content, as wide as the label (size, with scaled off).

Different SizesDifferent Sizes

With Icons​

Options with an icon each, so an action is recognised before its label is read; pick one and its icon moves into the button next to the label (icon on each option).

With IconsWith Icons

With Option Descriptions​

Options whose label alone does not explain the choice: open the list and each row shows the label with a line of explanation under it (description on each option). The selected row is highlighted and stays clickable (displaySelectedOption).

With Option DescriptionsWith Option Descriptions

Disabled​

A choice that cannot be changed right now, kept on screen so the reader still sees the value: the button is greyed out and a click does not open the list (isDisabled).

DisabledDisabled

With Selected Option​

A form that opens with a value already chosen. Open the list: the chosen option is greyed out and cannot be picked again, because displaySelectedOption is off; turn it on to keep it clickable and highlighted instead.

With Selected OptionWith Selected Option

Custom Styling​

Colour-coded values, such as priorities, where the colour says more than the word: the button draws the chosen option as a badge in its own text and background colours (type="badge", with color and backgroundColor on each option). Pick another priority to see the badge change; the rows of the list stay plain.

Custom StylingCustom Styling

Loading State​

A value that is still being fetched or saved: the label and the arrow give way to a spinner and a click does not open the list until loading ends (isLoading).

Loading StateLoading State

Right To Left​

The combo box under a right-to-left interface: the icon and the label start at the right edge, the icon is mirrored, and the arrow sits at the left end. Open the list to see its rows aligned to the right as well. The direction comes from the theme's interfaceDirection (the Direction toolbar); the wrapper also carries dir="rtl" for the rules that read the DOM direction.

Right To LeftRight To Left

Css Customization​

The border colours, the radius and the padding set on one wrapper -- the variables are listed under CSS variables on this page. Hover the button to see the hover colour and click it to see the open one.

Css CustomizationCss Customization

Minimal example​

selectedOption and options are both required, and onSelect is what updates the first.

import { useState } from "react";
import {
ComboBox,
type TOption,
} from "@onlyoffice/apps-ui-kit/components/combobox";

const ROLES: TOption[] = [
{ key: "viewer", label: "Viewer" },
{ key: "editor", label: "Editor" },
{ key: "owner", label: "Owner" },
];

export function RolePicker() {
const [role, setRole] = useState<TOption>(ROLES[0]);

return (
<div style={{ width: 240 }}>
<ComboBox
options={ROLES}
selectedOption={role}
onSelect={setRole}
scaled
scaledOptions
/>
</div>
);
}

Props​

PropertyTypeDescription
optionsTOption[]The options. Each needs a unique key, and a label unless it is a separator; disabled, icon, description, isBeta and tooltip are passed to the row.
selectedOptionTOptionThe option to show in the button. The component does not choose it: keep it in your own state and set it from onSelect.
advancedOptions?ReactElement<{ children?: React.ReactNode; }, string | JSXElementConstructor<any>>Element whose children replace the list entirely, for a menu that is not a list of options. options is then used only for the button.
advancedOptionsCount?numberHow many advanced options there are, for deciding the mobile layout.
children?ReactNodeContent rendered inside the button, before the label. A click on it is ignored unless disableIconClick is false.
className?stringApplied to the element that wraps the button and the list.
comboIcon?ReactNodeIcon to draw instead of the arrow: a component, an element, or a URL.
dataTestId?stringValue of data-testid on the wrapper. Default: "combobox".
directionX?TDirectionXPreferred horizontal side of the list; it flips to fit the viewport.
directionY?TDirectionYPreferred vertical side of the list; it flips to fit the viewport.
disableIconClick?booleanWhether a click on children is swallowed instead of opening the list. Default: true.
disableItemClick?booleanStops the list opening at all, without the disabled styling.
disableItemClickFirstLevel?booleanThe same, but only for first-level items on a touch device.
displayArrow?booleanDraws the arrow even when there are no options to open.
displaySelectedOption?booleanKeeps the option that is currently selected usable and highlights it. Without it that option is rendered disabled, so the value cannot be picked again.
displayType?ComboBoxDisplayTypetoggle renders the button alone and no list at all, for a control that only looks like a combo box. Default: ComboBoxDisplayType.default.
fillIcon?booleanRecolours the selected option's icon to the text colour.
fixedDirection?booleanKeeps directionX and directionY as given instead of flipping them.
forceCloseClickOutside?booleanStops the outside-click listener being registered.
hideMobileView?booleanKeeps the list anchored to the button on a phone instead of turning it into a bottom sheet.
id?stringApplied to the element that wraps the button and the list.
isAside?booleanMarks the list's backdrop as belonging to a side panel.
isDefaultMode?booleanWhether the list is rendered in a portal on document.body. Default: true.
isDisabled?booleanGreys the button out and stops it opening.
isLoading?booleanReplaces the arrow with a spinner and stops the button opening.
isMobileView?booleanPins the list to the bottom of the screen, full width, in portrait.
isNoFixedHeightOptions?booleanRenders the options in a plain scrollbar instead of the virtualised list.
manualWidth?stringWidth of the list as a CSS length. It has nothing to do with the button's width — use scaledOptions for that. Default: "200px".
manualX?string(Non-portal mode) Exact horizontal offset of the list from the button.
manualY?number | string(Non-portal mode) Exact vertical offset of the list from the button.
modernView?booleanCompact button with no background until it is open.
noBorder?booleanRemoves the button's border.
noSelect?booleanWhether the button's text cannot be selected. Default: true.
offsetX?numberHorizontal offset of the list, in pixels.
onBackdropClick?(e: Event) => voidCalled when a click outside closes the list, if withBackdrop is on.
onClickSelectedItem?(option: TOption) => voidCalled when the option already selected is clicked again.
onSelect?(option: TOption) => voidCalled with the option that was clicked. Nothing changes on its own — selectedOption is yours to update.
onToggle?(e: React.MouseEvent<HTMLDivElement>, isOpen: boolean) => voidCalled when the button is clicked, with the state being asked for. Passing it without onBackdropClick also stops a click outside closing the list.
opened?booleanOpens or closes the list from outside. It seeds the internal state rather than controlling it: the next click on the button wins until this changes.
optionStyle?CSSPropertiesInline style applied to every option.
plusBadgeValue?numberNumber shown as +N after the label, for a multi-select summary.
role?stringIgnored. Nothing reads this prop.
scaled?booleanMakes the button take the full width of its parent, which overrides size. Default: true.
scaledOptions?booleanMatches the list's width to the button's instead of manualWidth.
searchPlaceholder?stringIgnored. Nothing reads this prop; there is no search field.
setIsOpenItemAccess?(isOpen: boolean) => voidCalled with the open state whenever it changes, alongside onToggle.
shouldShowBackdrop?booleanRenders the backdrop even when another one is already on screen.
showDisabledItems?booleanIgnored. The list is always told to keep disabled options.
size?"base" | "big" | "content" | "huge" | "middle"One of the fixed widths — 173, 300, 350 or 500px, or the content's own. It only applies when scaled is false. Default: ComboBoxSize.base.
style?CSSPropertiesApplied to the wrapper and, again, to the list.
tabIndex?numberPosition of the button in the tab order. It is 0 by default, so the control is reachable; pass -1 to take it off the tab order. Default: 0.
textOverflow?booleanTruncates an option's label with an ellipsis instead of wrapping it.
title?stringHover tooltip for the whole control. It needs RootTooltip mounted.
topSpace?numberSpace to leave above the list when it opens upwards, in pixels.
type?TComboboxShape of the button: badge draws the label as a coloured badge, onlyIcon drops the label, descriptive adds the option's description under it.
useImageIcon?booleanDraws the kit's placeholder image next to the selected option's icon.
usePortalBackdrop?booleanMoves the list's backdrop into the portal, above the page.
withBackdrop?booleanWhether the list renders a backdrop to catch the next click. Default: true.
withBackground?booleanGives that backdrop its dimming background.
withBlur?booleanIgnored. It reaches the list, which does not read it either.
withLabel?booleanWhether the selected option is matched by label rather than by key. Default: true.
withoutArrow?booleanHides the arrow, whatever displayArrow and the options say.
withoutBackground?booleanMakes that backdrop transparent.
withoutPadding?booleanRemoves the vertical padding around the button.
withSearch?booleanIgnored. Nothing reads this prop; there is no search field.

Enums​

EnumMembers
ComboBoxDisplayTypedefault, toggle

Recipes​

Open and close, controlled​

opened seeds the open state rather than owning it, and onToggle reports every click on the button. Pass onBackdropClick as well, or a click outside stops closing the list.

import { useState } from "react";
import {
ComboBox,
type TOption,
} from "@onlyoffice/apps-ui-kit/components/combobox";

const SORTS: TOption[] = [
{ key: "name", label: "Name" },
{ key: "date", label: "Last modified" },
];

export function SortPicker() {
const [sort, setSort] = useState<TOption>(SORTS[0]);
const [open, setOpen] = useState(false);

return (
<ComboBox
options={SORTS}
selectedOption={sort}
opened={open}
onToggle={(_event, next) => setOpen(next)}
onBackdropClick={() => setOpen(false)}
onSelect={(option) => {
setSort(option);
setOpen(false);
}}
/>
);
}

Loading and disabled​

import {
ComboBox,
type TOption,
} from "@onlyoffice/apps-ui-kit/components/combobox";

const PLACEHOLDER: TOption = { key: "none", label: "Loading…" };

export function RoomPicker({
rooms,
isLoading,
}: {
rooms: TOption[];
isLoading: boolean;
}) {
return (
<ComboBox
options={rooms}
selectedOption={rooms[0] ?? PLACEHOLDER}
isLoading={isLoading}
isDisabled={rooms.length === 0}
onSelect={() => {}}
/>
);
}

A long list that scrolls​

dropDownMaxHeight is what gives the list a scrollbar; without it every option is rendered.

import { useState } from "react";
import {
ComboBox,
type TOption,
} from "@onlyoffice/apps-ui-kit/components/combobox";

export function TimezonePicker({ zones }: { zones: TOption[] }) {
const [zone, setZone] = useState<TOption>(zones[0]);

return (
<ComboBox
options={zones}
selectedOption={zone}
onSelect={setZone}
dropDownMaxHeight={320}
displaySelectedOption
textOverflow
/>
);
}

Behaviour the types don't state​

  • The selected option is matched by its label, not its key. withLabel is true by default, and the comparison is option.label === selectedOption.label, so two options that read the same are both treated as selected. Pass withLabel={false} to compare keys instead.
  • The option you are on is rendered disabled. Unless displaySelectedOption is set, the option matching the selection is greyed out and cannot be clicked, which is how the kit shows the current value — and why re-picking it does nothing.
  • Nothing selects anything. onSelect hands you the option; selectedOption is yours to update, and the component mirrors whatever you pass back.
  • The list is 200px wide whatever the button is. manualWidth defaults to "200px"; scaledOptions is what matches it to the button, and even then the width is measured from the button on a later render, so the first open can still be 200px.
  • scaled is on by default, so the control fills its parent and size has no effect. Turn it off to get the 173px of ComboBoxSize.base.
  • The keyboard does not work. The component listens for ArrowDown and Enter on the document and looks for options by the test id drop-down-item — which its own options never carry, because it gives each one a test id of its own. The result is that the arrows and Enter do nothing while the list is open, and Enter is swallowed for the rest of the page as well.
  • Loading hides the choice. isLoading draws a spinner in the button and hides its label, icon, children and arrow while keeping their space, so the button keeps its width but no longer shows the current value.
  • A list of four or more options becomes a bottom sheet on a phone. hideMobileView keeps it anchored to the button.
  • An option's icon has to be a component or a URL: an element is ignored by the button, and a URL is fetched at runtime by react-svg.
  • searchPlaceholder, withSearch, showDisabledItems, withBlur and role are declared and never read. Disabled options are always kept in the list.
  • The component is memoised with a deep comparison of all its props, so a new options array on every render costs a full walk of it rather than a re-render.

Sub-components​

ComboButton is the button alone, exported for a control that needs the same shape without a list of its own. It takes the selectedOption, size, type, isOpen and isLoading props described above and calls onClick; everything else — opening, choosing, closing — is yours.

CSS variables​

VariableDefaultEffect
--combobox-border-colortheme greyBorder colour of the button
--combobox-hover-border-colortheme greyBorder colour of the button under the pointer
--combobox-focus-border-colorthe accent colour (white in dark theme)Border colour of the button while its list is open, hovered or not
--combobox-radius3pxCorner radius of the button when it has a border
--combobox-inner-padding4px 0Space above and below the button, inside the wrapper
--combobox-base-width173pxWidth of the wrapper at ComboBoxSize.base
--combobox-middle-width300pxWidth of the wrapper at ComboBoxSize.middle
--combobox-big-width350pxWidth of the wrapper at ComboBoxSize.big
--combobox-huge-width500pxWidth of the wrapper at ComboBoxSize.huge

The four widths only apply with scaled off, and they size the wrapper, not the button: the button keeps its own fixed 173, 300, 350 or 500px, so a larger value widens only the empty area beside it. Under noBorder the button has no border and so no radius.

--combobox-bg and --combobox-open-bg are read by the stylesheet and paint nothing: the rule that uses them waits for a noBorder class on the wrapper, which the component never sets.

The list is a DropDown and takes that component's variables.

Accessibility​

  • The button is a <div role="button"> with aria-haspopup="listbox", and aria-expanded and aria-pressed that are both true while the list is open; aria-disabled follows isDisabled. Its tabIndex defaults to 0, so it is reachable. It used to default to -1, which left the control off the keyboard entirely. Focus gets there, but only a click opens the list: Enter and Space on the button do nothing, Escape does not close it, and the arrow keys do not move through it, as above.
  • The options are role="option" rows in a role="listbox" that is not linked to the button by aria-controls or aria-activedescendant.
  • The control's name comes from its content, the selected option's label, which says the value but not what is being chosen. Put it in a FieldContainer, or give it an aria-label through a wrapper.
  • title renders the kit's hover tooltip, which is not an accessible name and needs RootTooltip mounted.

Test ids​

Elementdata-testid
The controlcombobox, overridable with dataTestId
The listset by dropDownTestId, otherwise dropdown
An optionthe option's dataTestId, otherwise drop_down_item_<key>

The button inside carries data-test-id="combo-button" — with hyphens, unlike everything else in the kit — and so do its icon, badge and arrow.