alpha

ComboBox

A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query.

installyarn add react-aria-components
version1.0.0-alpha.0
usageimport {ComboBox} from 'react-aria-components'

Example#


import {Button, ComboBox, Input, Item, Label, ListBox, Popover} from 'react-aria-components';

<ComboBox>
  <Label>Favorite Animal</Label>
  <div>
    <Input />
    <Button></Button>
  </div>
  <Popover>
    <ListBox>
      <Item>Aardvark</Item>
      <Item>Cat</Item>
      <Item>Dog</Item>
      <Item>Kangaroo</Item>
      <Item>Panda</Item>
      <Item>Snake</Item>
    </ListBox>
  </Popover>
</ComboBox>
import {
  Button,
  ComboBox,
  Input,
  Item,
  Label,
  ListBox,
  Popover
} from 'react-aria-components';

<ComboBox>
  <Label>Favorite Animal</Label>
  <div>
    <Input />
    <Button></Button>
  </div>
  <Popover>
    <ListBox>
      <Item>Aardvark</Item>
      <Item>Cat</Item>
      <Item>Dog</Item>
      <Item>Kangaroo</Item>
      <Item>Panda</Item>
      <Item>Snake</Item>
    </ListBox>
  </Popover>
</ComboBox>
import {
  Button,
  ComboBox,
  Input,
  Item,
  Label,
  ListBox,
  Popover
} from 'react-aria-components';

<ComboBox>
  <Label>
    Favorite Animal
  </Label>
  <div>
    <Input />
    <Button></Button>
  </div>
  <Popover>
    <ListBox>
      <Item>
        Aardvark
      </Item>
      <Item>Cat</Item>
      <Item>Dog</Item>
      <Item>
        Kangaroo
      </Item>
      <Item>
        Panda
      </Item>
      <Item>
        Snake
      </Item>
    </ListBox>
  </Popover>
</ComboBox>
Show CSS
.react-aria-ComboBox {
  .react-aria-Input {
    margin: 0;
    font-size: 1.072rem;
    border: 1px solid var(--spectrum-global-color-gray-400);
    border-radius: 6px;
    padding: 0.286rem 2rem 0.286rem 0.571rem;
    vertical-align: middle;

    &[aria-invalid] {
      border-color: var(--spectrum-global-color-red-600);
    }

    &:focus {
      outline: none;
      border-color: slateblue;
      box-shadow: 0 0 0 1px slateblue;
    }
  }

  .react-aria-Button {
    background: slateblue;
    color: white;
    border-radius: 4px;
    border: none;
    appearance: none;
    margin-left: -1.714rem;
    width: 1.429rem;
    height: 1.429rem;
    padding: 0;
    vertical-align: middle;
    font-size: 0.857rem;
    outline: none;
  }

  [slot=description] {
    font-size: 12px;
  }

  [slot=errorMessage] {
    font-size: 12px;
    color: var(--spectrum-global-color-red-600);
  }
}

.react-aria-ListBox {
  --highlight-background: slateblue;
  --highlight-foreground: white;
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  max-height: inherit;
  overflow: auto;
  padding: 2px;
  outline: none;

  .react-aria-Section:not(:first-child) {
    margin-top: 12px;
  }

  .react-aria-Header {
    font-size: 1.143rem;
    font-weight: bold;
    padding: 0 0.571rem 0 1.571rem;
  }

  .react-aria-Item {
    margin: 2px;
    padding: 0.286rem 0.571rem 0.286rem 1.571rem;
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    display: flex;
    flex-direction: column;

    &[aria-selected=true] {
      font-weight: 600;

      &::before {
        content: '✓';
        content: '✓' / '';
        alt: ' ';
        position: absolute;
        top: 4px;
        left: 4px;
      }
    }

    &[data-focused],
    &[data-pressed] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
    }

    &[aria-disabled] {
      color: var(--text-color-disabled);
    }

    [slot=label] {
      font-weight: bold;
    }

    [slot=description] {
      font-size: small;
    }
  }
}

.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  width: var(--trigger-width);
  box-sizing: border-box;
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;
}

@media (forced-colors: active) {
  .react-aria-ComboBox {
    .react-aria-Button {
      background: ButtonFace;
      color: ButtonText;
      border: 1px solid ButtonBorder;
    }
  }

  .react-aria-ListBox {
    forced-color-adjust: none;

    --highlight-background: Highlight;
    --highlight-foreground: HighlightText;
    --border-color: ButtonBorder;
    --background-color: ButtonFace;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
  }
}
.react-aria-ComboBox {
  .react-aria-Input {
    margin: 0;
    font-size: 1.072rem;
    border: 1px solid var(--spectrum-global-color-gray-400);
    border-radius: 6px;
    padding: 0.286rem 2rem 0.286rem 0.571rem;
    vertical-align: middle;

    &[aria-invalid] {
      border-color: var(--spectrum-global-color-red-600);
    }

    &:focus {
      outline: none;
      border-color: slateblue;
      box-shadow: 0 0 0 1px slateblue;
    }
  }

  .react-aria-Button {
    background: slateblue;
    color: white;
    border-radius: 4px;
    border: none;
    appearance: none;
    margin-left: -1.714rem;
    width: 1.429rem;
    height: 1.429rem;
    padding: 0;
    vertical-align: middle;
    font-size: 0.857rem;
    outline: none;
  }

  [slot=description] {
    font-size: 12px;
  }

  [slot=errorMessage] {
    font-size: 12px;
    color: var(--spectrum-global-color-red-600);
  }
}

.react-aria-ListBox {
  --highlight-background: slateblue;
  --highlight-foreground: white;
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  max-height: inherit;
  overflow: auto;
  padding: 2px;
  outline: none;

  .react-aria-Section:not(:first-child) {
    margin-top: 12px;
  }

  .react-aria-Header {
    font-size: 1.143rem;
    font-weight: bold;
    padding: 0 0.571rem 0 1.571rem;
  }

  .react-aria-Item {
    margin: 2px;
    padding: 0.286rem 0.571rem 0.286rem 1.571rem;
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    display: flex;
    flex-direction: column;

    &[aria-selected=true] {
      font-weight: 600;

      &::before {
        content: '✓';
        content: '✓' / '';
        alt: ' ';
        position: absolute;
        top: 4px;
        left: 4px;
      }
    }

    &[data-focused],
    &[data-pressed] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
    }

    &[aria-disabled] {
      color: var(--text-color-disabled);
    }

    [slot=label] {
      font-weight: bold;
    }

    [slot=description] {
      font-size: small;
    }
  }
}

.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  width: var(--trigger-width);
  box-sizing: border-box;
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;
}

@media (forced-colors: active) {
  .react-aria-ComboBox {
    .react-aria-Button {
      background: ButtonFace;
      color: ButtonText;
      border: 1px solid ButtonBorder;
    }
  }

  .react-aria-ListBox {
    forced-color-adjust: none;

    --highlight-background: Highlight;
    --highlight-foreground: HighlightText;
    --border-color: ButtonBorder;
    --background-color: ButtonFace;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
  }
}
.react-aria-ComboBox {
  .react-aria-Input {
    margin: 0;
    font-size: 1.072rem;
    border: 1px solid var(--spectrum-global-color-gray-400);
    border-radius: 6px;
    padding: 0.286rem 2rem 0.286rem 0.571rem;
    vertical-align: middle;

    &[aria-invalid] {
      border-color: var(--spectrum-global-color-red-600);
    }

    &:focus {
      outline: none;
      border-color: slateblue;
      box-shadow: 0 0 0 1px slateblue;
    }
  }

  .react-aria-Button {
    background: slateblue;
    color: white;
    border-radius: 4px;
    border: none;
    appearance: none;
    margin-left: -1.714rem;
    width: 1.429rem;
    height: 1.429rem;
    padding: 0;
    vertical-align: middle;
    font-size: 0.857rem;
    outline: none;
  }

  [slot=description] {
    font-size: 12px;
  }

  [slot=errorMessage] {
    font-size: 12px;
    color: var(--spectrum-global-color-red-600);
  }
}

.react-aria-ListBox {
  --highlight-background: slateblue;
  --highlight-foreground: white;
  --text-color: var(--spectrum-alias-text-color);
  --text-color-disabled: var(--spectrum-alias-text-color-disabled);

  max-height: inherit;
  overflow: auto;
  padding: 2px;
  outline: none;

  .react-aria-Section:not(:first-child) {
    margin-top: 12px;
  }

  .react-aria-Header {
    font-size: 1.143rem;
    font-weight: bold;
    padding: 0 0.571rem 0 1.571rem;
  }

  .react-aria-Item {
    margin: 2px;
    padding: 0.286rem 0.571rem 0.286rem 1.571rem;
    border-radius: 6px;
    outline: none;
    cursor: default;
    color: var(--text-color);
    font-size: 1.072rem;
    position: relative;
    display: flex;
    flex-direction: column;

    &[aria-selected=true] {
      font-weight: 600;

      &::before {
        content: '✓';
        content: '✓' / '';
        alt: ' ';
        position: absolute;
        top: 4px;
        left: 4px;
      }
    }

    &[data-focused],
    &[data-pressed] {
      background: var(--highlight-background);
      color: var(--highlight-foreground);
    }

    &[aria-disabled] {
      color: var(--text-color-disabled);
    }

    [slot=label] {
      font-weight: bold;
    }

    [slot=description] {
      font-size: small;
    }
  }
}

.react-aria-Popover {
  --background-color: var(--page-background);
  --border-color: var(--spectrum-global-color-gray-400);

  border: 1px solid var(--border-color);
  width: var(--trigger-width);
  box-sizing: border-box;
  box-shadow: 0 8px 20px rgba(0 0 0 / 0.1);
  border-radius: 6px;
  background: var(--background-color);
  outline: none;
}

@media (forced-colors: active) {
  .react-aria-ComboBox {
    .react-aria-Button {
      background: ButtonFace;
      color: ButtonText;
      border: 1px solid ButtonBorder;
    }
  }

  .react-aria-ListBox {
    forced-color-adjust: none;

    --highlight-background: Highlight;
    --highlight-foreground: HighlightText;
    --border-color: ButtonBorder;
    --background-color: ButtonFace;
    --text-color: ButtonText;
    --text-color-disabled: GrayText;
  }
}

Features#


A combo box can be built using the <datalist> HTML element, but this is very limited in functionality and difficult to style. ComboBox helps achieve accessible combo box and autocomplete components that can be styled as needed.

  • Flexible – Support for selecting pre-defined values, custom values, controlled and uncontrolled state, custom filter functions, async loading, disabled items, validation, and multiple menu trigger options.
  • Keyboard navigation – ComboBox can be opened and navigated using the arrow keys, along with page up/down, home/end, etc. The list of options is filtered while typing into the input, and items can be selected with the enter key.
  • Accessible – Follows the ARIA combobox pattern, with support for items and sections, and slots for label and description elements within each item. Custom localized announcements are included for option focusing, filtering, and selection using an ARIA live region to ensure announcements are clear and consistent.
  • Styleable – Items include builtin states for styling, such as hover, press, focus, selected, and disabled.

Read our blog post for more details about the interactions and accessibility features implemented by ComboBox.

Anatomy#


LabelLabelInputButtonLabelOption oneOption twoOption threeOpList boxOp

A combo box consists of a label, an input which displays the current value, a list box popup, and an optional button used to toggle the list box popup open state. Users can type within the input to filter the available options within the list box. The list box popup may be opened by a variety of input field interactions specified by the menuTrigger prop provided to ComboBox, or by clicking or touching the button.

ComboBox also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the aria-describedby attribute.

If the combo box does not have a visible label, an aria-label or aria-labelledby prop must be passed instead to identify it to assistive technology.

Concepts#

ComboBox makes use of the following concepts:

Composed components#

A ComboBox uses the following components, which may also be used standalone or reused in other components.

Label
A label provides context for an input element.
Input
An input allows a user to enter a plain text value with a keyboard.
Button
A button allows a user to perform an action.
Popover
A popover displays content in context with a trigger element.
ListBox
A listbox allows a user to select one or more options from a list.

Props#


ComboBox#

NameTypeDefaultDescription
defaultFilter( (textValue: string, , inputValue: string )) => booleanThe filter function used to determine if a option should be included in the combo box list.
shouldFocusWrapbooleanWhether keyboard navigation is circular.
defaultItemsIterable<T>The list of ComboBox items (uncontrolled).
itemsIterable<T>The list of ComboBox items (controlled).
inputValuestringThe value of the ComboBox input (controlled).
defaultInputValuestringThe default value of the ComboBox input (uncontrolled).
allowsCustomValuebooleanWhether the ComboBox allows a non-item matching input value to be set.
menuTriggerMenuTriggerAction'input'The interaction required to display the ComboBox menu.
disabledKeysIterable<Key>The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectedKeyKeynullThe currently selected key in the collection (controlled).
defaultSelectedKeyKeyThe initial selected key in the collection (uncontrolled).
isDisabledbooleanWhether the input is disabled.
isReadOnlybooleanWhether the input can be selected but not changed by the user.
validationStateValidationStateWhether the input should display its "valid" or "invalid" visual styling.
isRequiredboolean

Whether user input is required on the input before form submission. Often paired with the necessityIndicator prop to add a visual indicator to the input.

autoFocusbooleanWhether the element should receive focus on render.
childrenReactNode( (values: ComboBoxState<object> )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: ComboBoxState<object> )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: ComboBoxState<object> )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Events
NameTypeDefaultDescription
onOpenChange( (isOpen: boolean, , menuTrigger?: MenuTriggerAction )) => voidMethod that is called when the open state of the menu changes. Returns the new open state and the action that caused the opening of the menu.
onInputChange( (value: string )) => voidHandler that is called when the ComboBox input value changes.
onSelectionChange( (key: Key )) => anyHandler that is called when the selection changes.
onFocus( (e: FocusEvent<HTMLInputElement> )) => voidHandler that is called when the element receives focus.
onBlur( (e: FocusEvent<HTMLInputElement> )) => voidHandler that is called when the element loses focus.
onFocusChange( (isFocused: boolean )) => voidHandler that is called when the element's focus status changes.
onKeyDown( (e: KeyboardEvent )) => voidHandler that is called when a key is pressed.
onKeyUp( (e: KeyboardEvent )) => voidHandler that is called when a key is released.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Label#

A <Label> accepts all props supported by the <label> HTML element.

Input#

An <Input> accepts all props supported by the <input> HTML element.

Button#

A <Button> accepts its contents as children. Other props such as onPress and isDisabled will be set by the ComboBox.

Show props
NameTypeDefaultDescription
isDisabledbooleanWhether the button is disabled.
autoFocusbooleanWhether the element should receive focus on render.
type'button''submit''reset''button'The behavior of the button when used in an HTML form.
childrenReactNode( (values: ButtonRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: ButtonRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: ButtonRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Events
NameTypeDefaultDescription
onPress( (e: PressEvent )) => voidHandler that is called when the press is released over the target.
onPressStart( (e: PressEvent )) => voidHandler that is called when a press interaction starts.
onPressEnd( (e: PressEvent )) => void

Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target.

onPressChange( (isPressed: boolean )) => voidHandler that is called when the press state changes.
onPressUp( (e: PressEvent )) => void

Handler that is called when a press is released over the target, regardless of whether it started on the target or not.

onFocus( (e: FocusEvent<Target> )) => voidHandler that is called when the element receives focus.
onBlur( (e: FocusEvent<Target> )) => voidHandler that is called when the element loses focus.
onFocusChange( (isFocused: boolean )) => voidHandler that is called when the element's focus status changes.
onKeyDown( (e: KeyboardEvent )) => voidHandler that is called when a key is pressed.
onKeyUp( (e: KeyboardEvent )) => voidHandler that is called when a key is released.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
excludeFromTabOrderboolean

Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available.

aria-expandedboolean'true''false'Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed.
aria-haspopupboolean'menu''listbox''tree''grid''dialog''true''false'Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element.
aria-controlsstringIdentifies the element (or elements) whose contents or presence are controlled by the current element.
aria-pressedboolean'true''false''mixed'Indicates the current "pressed" state of toggle buttons.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Popover#

A <Popover> is a container to hold the <ListBox> suggestions for a ComboBox. By default, it has a placement of bottom start within a <ComboBox>, but this and other positioning properties may be customized.

Show props
NameTypeDefaultDescription
triggerRefRefObject<Element>

The ref for the element which the popover positions itself with respect to.

When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone.

placementPlacement'bottom'The placement of the element with respect to its anchor element.
containerPaddingnumber12

The placement padding that should be applied between the element and its surrounding container.

offsetnumber0

The additional offset applied along the main axis between the element and its anchor element.

crossOffsetnumber0

The additional offset applied along the cross axis between the element and its anchor element.

shouldFlipbooleantrue

Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely.

isNonModalboolean

Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies.

Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully.

isKeyboardDismissDisabledbooleanfalse

Whether pressing the escape key to close the popover should be disabled.

Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided.

arrowSizenumber0Cross size of the overlay arrow in pixels.
boundaryElementElementdocument.bodyElement that that serves as the positioning boundary.
scrollRefRefObject<Element>overlayRefA ref for the scrollable region within the overlay.
shouldUpdatePositionbooleantrueWhether the overlay should update its position automatically.
arrowBoundaryOffsetnumber0The minimum distance the arrow's edge should be from the edge of the overlay element.
childrenReactNode( (values: PopoverRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: PopoverRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: PopoverRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Sizing
NameTypeDefaultDescription
maxHeightnumber

The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height.

ListBox#

Within a <ComboBox>, most <ListBox> props are set automatically. The <ListBox> defines the options to display in a ComboBox.

Show props
NameTypeDefaultDescription
selectionBehaviorSelectionBehaviorHow multiple selection should behave in the collection.
dragAndDropHooksDragAndDropHooksThe drag and drop hooks returned by useDragAndDrop used to enable drag and drop behavior for the ListBox.
renderEmptyState() => ReactNodeProvides content to display when there are no items in the list.
labelReactNodeAn optional visual label for the listbox.
autoFocusbooleanFocusStrategyWhether to auto focus the listbox or an option.
shouldFocusWrapbooleanWhether focus should wrap around when the end/start is reached.
itemsIterable<T>Item objects in the collection.
disabledKeysIterable<Key>The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectionModeSelectionModeThe type of selection that is allowed in the collection.
disallowEmptySelectionbooleanWhether the collection allows empty selection.
selectedKeys'all'Iterable<Key>The currently selected keys in the collection (controlled).
defaultSelectedKeys'all'Iterable<Key>The initial selected keys in the collection (uncontrolled).
childrenReactNode( (item: T )) => ReactElementThe contents of the collection.
classNamestring( (values: ListBoxRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: ListBoxRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Events
NameTypeDefaultDescription
onAction( (key: Key )) => void

Handler that is called when a user performs an action on an item. The exact user event depends on the collection's selectionBehavior prop and the interaction modality.

onSelectionChange( (keys: Selection )) => anyHandler that is called when the selection changes.
onFocus( (e: FocusEvent<Target> )) => voidHandler that is called when the element receives focus.
onBlur( (e: FocusEvent<Target> )) => voidHandler that is called when the element loses focus.
onFocusChange( (isFocused: boolean )) => voidHandler that is called when the element's focus status changes.
Layout
NameTypeDefaultDescription
slotstringA slot name for the component. Slots allow the component to receive props from a parent component.
Accessibility
NameTypeDefaultDescription
idstringThe element's unique identifier. See MDN.
aria-labelstringDefines a string value that labels the current element.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
aria-describedbystringIdentifies the element (or elements) that describes the object.
aria-detailsstringIdentifies the element (or elements) that provide a detailed, extended description for the object.

Section#

A <Section> defines the child items for a section within a <ListBox>. It may also contain an optional <Header> element. If there is no header, then an aria-label must be provided to identify the section to assistive technologies.

Show props
NameTypeDefaultDescription
valueobjectThe object value that this section represents. When using dynamic collections, this is set automatically.
childrenReactNode( (item: object )) => ReactElementStatic child items or a function to render children.
itemsIterable<T>Item objects in the section.
classNamestringThe CSS className for the element.
styleCSSPropertiesThe inline style for the element.
Accessibility
NameTypeDefaultDescription
idKeyThe unique id of the section.
aria-labelstringAn accessibility label for the section.

A <Header> defines the title for a <Section>. It accepts all DOM attributes.

Item#

An <Item> defines a single option within a <ListBox>. If the children are not plain text, then the textValue prop must also be set to a plain text representation, which will be used for autocomplete in the ComboBox.

Show props
NameTypeDefaultDescription
valueobjectThe object value that this item represents. When using dynamic collections, this is set automatically.
titleReactNodeRendered contents of the item if children contains child items.
textValuestringA string representation of the item's contents, used for features like typeahead.
childItemsIterable<T>A list of child item objects. Used for dynamic collections.
hasChildItemsbooleanWhether this item has children, even if not loaded yet.
childrenReactNode( (values: ItemRenderProps )) => ReactNodeThe children of the component. A function may be provided to alter the children based on component state.
classNamestring( (values: ItemRenderProps )) => stringThe CSS className for the element. A function may be provided to compute the class based on component state.
styleCSSProperties( (values: ItemRenderProps )) => CSSPropertiesThe inline style for the element. A function may be provided to compute the style based on component state.
Accessibility
NameTypeDefaultDescription
idKeyThe unique id of the item.
aria-labelstringAn accessibility label for this item.

Styling#


React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin className attribute which can be targeted using CSS selectors. These follow the react-aria-ComponentName naming convention.

.react-aria-ComboBox {
  /* ... */
}
.react-aria-ComboBox {
  /* ... */
}
.react-aria-ComboBox {
  /* ... */
}

A custom className can also be specified on any component. This overrides the default className provided by React Aria with your own.

<ComboBox className="my-combobox">
  {/* ... */}
</ComboBox>
<ComboBox className="my-combobox">
  {/* ... */}
</ComboBox>
<ComboBox className="my-combobox">
  {/* ... */}
</ComboBox>

In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using DOM attributes, which you can target in CSS selectors. These are ARIA attributes wherever possible, or data attributes when a relevant ARIA attribute does not exist. For example:

.react-aria-Item[aria-selected=true] {
  /* ... */
}

.react-aria-Item[data-focused] {
  /* ... */
}
.react-aria-Item[aria-selected=true] {
  /* ... */
}

.react-aria-Item[data-focused] {
  /* ... */
}
.react-aria-Item[aria-selected=true] {
  /* ... */
}

.react-aria-Item[data-focused] {
  /* ... */
}

The className and style props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind.

<Item
  className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
  Item
</Item>
<Item
  className={({ isSelected }) =>
    isSelected ? 'bg-blue-400' : 'bg-gray-100'}
>
  Item
</Item>
<Item
  className={(
    { isSelected }
  ) =>
    isSelected
      ? 'bg-blue-400'
      : 'bg-gray-100'}
>
  Item
</Item>

Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render a checkmark icon when an item is selected.

<Item>
  {({isSelected}) => (
    <>
      {isSelected && <CheckmarkIcon />}
      Item
    </>
  )}
</Item>
<Item>
  {({isSelected}) => (
    <>
      {isSelected && <CheckmarkIcon />}
      Item
    </>
  )}
</Item>
<Item>
  {(
    { isSelected }
  ) => (
    <>
      {isSelected && (
        <CheckmarkIcon />
      )}
      Item
    </>
  )}
</Item>

The states and selectors for each component used in a ComboBox are documented below.

ComboBox#

A ComboBox can be targeted with the .react-aria-ComboBox CSS selector, or by overriding with a custom className. It provides a ComboBoxState object to its render props, which can be used to customize the className, style, or children.

Label#

A Label can be targeted with the .react-aria-Label CSS selector, or by overriding with a custom className.

Input#

An Input within a ComboBox can be targeted with the .react-aria-Input CSS selector, or by overriding with a custom className. It supports standard CSS pseudo classes such as :focus for states. See MDN for details.

Button#

A Button can be targeted with the .react-aria-Button CSS selector, or by overriding with a custom className. It supports the following states:

NameCSS SelectorDescription
isHovered[data-hovered]Whether the button is currently hovered with a mouse.
isPressed[data-pressed]Whether the button is currently in a pressed state.
isFocused:focusWhether the button is focused, either via a mouse or keyboard.
isFocusVisible[data-focus-visible]Whether the button is keyboard focused.
isDisabled:disabledWhether the button is disabled.

Popover#

The Popover component can be targeted with the .react-aria-Popover CSS selector, or by overriding with a custom className. Note that it renders in a React Portal, so it will not appear as a descendant of the ComboBox in the DOM.

The --trigger-width CSS custom property will be set on the popover, which you can use to make the popover match the width of the combobox.

.react-aria-Popover {
  width: var(--trigger-width);
}
.react-aria-Popover {
  width: var(--trigger-width);
}
.react-aria-Popover {
  width: var(--trigger-width);
}

ListBox#

A ListBox can be targeted with the .react-aria-ListBox CSS selector, or by overriding with a custom className.

Section#

A Section can be targeted with the .react-aria-Section CSS selector, or by overriding with a custom className. See sections for examples.

Header#

A Header within a Section can be targeted with the .react-aria-Header CSS selector, or by overriding with a custom className. See sections for examples.

Item#

An Item can be targeted with the .react-aria-Item CSS selector, or by overriding with a custom className. It supports the following states and render props:

NameCSS SelectorDescription
isHovered[data-hovered]Whether the item is currently hovered with a mouse.
isPressed[data-pressed]Whether the item is currently in a pressed state.
isSelected[aria-selected=true]Whether the item is currently selected.
isFocused[data-focused]Whether the item is currently focused.
isFocusVisible[data-focus-visible]Whether the item is currently keyboard focused.
isDisabled[aria-disabled]

Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on disabledKeys and disabledBehavior.

selectionModeThe type of selection that is allowed in the collection.
selectionBehaviorThe selection behavior for the collection.

Items also support two slots: a label, and a description. When provided using the <Text> element, the item will have aria-labelledby and aria-describedby attributes pointing to these slots, improving screen reader announcement. See complex items for an example.

Note that items may not contain interactive children such as buttons, as screen readers will not be able to access them.

Text#

The help text elements within a ComboBox can be targeted with the [slot=description] and [slot=errorMessage] CSS selectors, or by adding a custom className.

Reusable wrappers#


If you will use a ComboBox in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency.

This example wraps ComboBox and all of its children together into a single component which accepts a label prop and children, which are passed through to the right places. It also shows how to use the description and errorMessage slots to render help text (see below for details). The Item component is also wrapped to apply class names based on the current state, as described above.

import type {ComboBoxProps, ItemProps} from 'react-aria-components';
import {Text} from 'react-aria-components';

interface MyComboBoxProps<T extends object> extends ComboBoxProps<T> {
  label?: string;
  description?: string | null;
  errorMessage?: string | null;
  children: React.ReactNode | ((item: T) => React.ReactNode);
}

function MyComboBox<T extends object>(
  { label, description, errorMessage, children, ...props }: MyComboBoxProps<T>
) {
  return (
    <ComboBox {...props}>
      <Label>{label}</Label>
      <div className="my-combobox-container">
        <Input />
        <Button></Button>
      </div>
      {description && <Text slot="description">{description}</Text>}
      {errorMessage && <Text slot="errorMessage">{errorMessage}</Text>}
      <Popover>
        <ListBox>
          {children}
        </ListBox>
      </Popover>
    </ComboBox>
  );
}

function MyItem(props: ItemProps) {
  return (
    <Item
      {...props}
      className={({ isFocused, isSelected }) =>
        `my-item ${isFocused ? 'focused' : ''} ${isSelected ? 'selected' : ''}`}
    />
  );
}

<MyComboBox label="Ice cream flavor">
  <MyItem>Chocolate</MyItem>
  <MyItem>Mint</MyItem>
  <MyItem>Strawberry</MyItem>
  <MyItem>Vanilla</MyItem>
</MyComboBox>
import type {
  ComboBoxProps,
  ItemProps
} from 'react-aria-components';
import {Text} from 'react-aria-components';

interface MyComboBoxProps<T extends object>
  extends ComboBoxProps<T> {
  label?: string;
  description?: string | null;
  errorMessage?: string | null;
  children:
    | React.ReactNode
    | ((item: T) => React.ReactNode);
}

function MyComboBox<T extends object>(
  { label, description, errorMessage, children, ...props }:
    MyComboBoxProps<T>
) {
  return (
    <ComboBox {...props}>
      <Label>{label}</Label>
      <div className="my-combobox-container">
        <Input />
        <Button></Button>
      </div>
      {description && (
        <Text slot="description">{description}</Text>
      )}
      {errorMessage && (
        <Text slot="errorMessage">{errorMessage}</Text>
      )}
      <Popover>
        <ListBox>
          {children}
        </ListBox>
      </Popover>
    </ComboBox>
  );
}

function MyItem(props: ItemProps) {
  return (
    <Item
      {...props}
      className={({ isFocused, isSelected }) =>
        `my-item ${isFocused ? 'focused' : ''} ${
          isSelected ? 'selected' : ''
        }`}
    />
  );
}

<MyComboBox label="Ice cream flavor">
  <MyItem>Chocolate</MyItem>
  <MyItem>Mint</MyItem>
  <MyItem>Strawberry</MyItem>
  <MyItem>Vanilla</MyItem>
</MyComboBox>
import type {
  ComboBoxProps,
  ItemProps
} from 'react-aria-components';
import {Text} from 'react-aria-components';

interface MyComboBoxProps<
  T extends object
> extends
  ComboBoxProps<T> {
  label?: string;
  description?:
    | string
    | null;
  errorMessage?:
    | string
    | null;
  children:
    | React.ReactNode
    | ((
      item: T
    ) =>
      React.ReactNode);
}

function MyComboBox<
  T extends object
>(
  {
    label,
    description,
    errorMessage,
    children,
    ...props
  }: MyComboBoxProps<T>
) {
  return (
    <ComboBox {...props}>
      <Label>
        {label}
      </Label>
      <div className="my-combobox-container">
        <Input />
        <Button></Button>
      </div>
      {description && (
        <Text slot="description">
          {description}
        </Text>
      )}
      {errorMessage && (
        <Text slot="errorMessage">
          {errorMessage}
        </Text>
      )}
      <Popover>
        <ListBox>
          {children}
        </ListBox>
      </Popover>
    </ComboBox>
  );
}

function MyItem(
  props: ItemProps
) {
  return (
    <Item
      {...props}
      className={(
        {
          isFocused,
          isSelected
        }
      ) =>
        `my-item ${
          isFocused
            ? 'focused'
            : ''
        } ${
          isSelected
            ? 'selected'
            : ''
        }`}
    />
  );
}

<MyComboBox label="Ice cream flavor">
  <MyItem>
    Chocolate
  </MyItem>
  <MyItem>Mint</MyItem>
  <MyItem>
    Strawberry
  </MyItem>
  <MyItem>
    Vanilla
  </MyItem>
</MyComboBox>
Show CSS
.my-item {
  margin: 2px;
  padding: 4px 8px 4px 22px;
  border-radius: 6px;
  outline: none;
  cursor: default;
  color: var(--text-color);
  font-size: 1.072rem;
  position: relative;

  &.selected {
    font-weight: 600;

    &::before {
      content: '✓';
      content: '✓' / '';
      alt: ' ';
      position: absolute;
      top: 4px;
      left: 4px;
    }
  }

  &.focused {
    background: #e70073;
    color: white;
  }
}

@media (forced-colors: active) {
  .my-item.focused {
    background: Highlight;
    color: HighlightText;
  }
}
.my-item {
  margin: 2px;
  padding: 4px 8px 4px 22px;
  border-radius: 6px;
  outline: none;
  cursor: default;
  color: var(--text-color);
  font-size: 1.072rem;
  position: relative;

  &.selected {
    font-weight: 600;

    &::before {
      content: '✓';
      content: '✓' / '';
      alt: ' ';
      position: absolute;
      top: 4px;
      left: 4px;
    }
  }

  &.focused {
    background: #e70073;
    color: white;
  }
}

@media (forced-colors: active) {
  .my-item.focused {
    background: Highlight;
    color: HighlightText;
  }
}
.my-item {
  margin: 2px;
  padding: 4px 8px 4px 22px;
  border-radius: 6px;
  outline: none;
  cursor: default;
  color: var(--text-color);
  font-size: 1.072rem;
  position: relative;

  &.selected {
    font-weight: 600;

    &::before {
      content: '✓';
      content: '✓' / '';
      alt: ' ';
      position: absolute;
      top: 4px;
      left: 4px;
    }
  }

  &.focused {
    background: #e70073;
    color: white;
  }
}

@media (forced-colors: active) {
  .my-item.focused {
    background: Highlight;
    color: HighlightText;
  }
}

Usage#


The following examples show how to use the MyComboBox component created in the above example.

Dynamic collections#

ComboBox follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time.

As seen below, an iterable list of options is passed to the ComboBox using the defaultItems prop. Each item accepts an id prop, which is passed to the onSelectionChange handler to identify the selected item. Alternatively, if the item objects contain an id property, as shown in the example below, then this is used automatically and an id prop is not required.

function Example() {
  let options = [
    {id: 1, name: 'Aerospace'},
    {id: 2, name: 'Mechanical'},
    {id: 3, name: 'Civil'},
    {id: 4, name: 'Biomedical'},
    {id: 5, name: 'Nuclear'},
    {id: 6, name: 'Industrial'},
    {id: 7, name: 'Chemical'},
    {id: 8, name: 'Agricultural'},
    {id: 9, name: 'Electrical'}
  ];
  let [majorId, setMajorId] = React.useState<React.Key | null>(null);

  return (
    <>
      <MyComboBox defaultItems={options} onSelectionChange={setMajorId}>
        {(item) => <Item>{item.name}</Item>}
      </MyComboBox>
      <p>Selected topic id: {majorId}</p>
    </>
  );
}
function Example() {
  let options = [
    { id: 1, name: 'Aerospace' },
    { id: 2, name: 'Mechanical' },
    { id: 3, name: 'Civil' },
    { id: 4, name: 'Biomedical' },
    { id: 5, name: 'Nuclear' },
    { id: 6, name: 'Industrial' },
    { id: 7, name: 'Chemical' },
    { id: 8, name: 'Agricultural' },
    { id: 9, name: 'Electrical' }
  ];
  let [majorId, setMajorId] = React.useState<
    React.Key | null
  >(null);

  return (
    <>
      <MyComboBox
        defaultItems={options}
        onSelectionChange={setMajorId}
      >
        {(item) => <Item>{item.name}</Item>}
      </MyComboBox>
      <p>Selected topic id: {majorId}</p>
    </>
  );
}
function Example() {
  let options = [
    {
      id: 1,
      name: 'Aerospace'
    },
    {
      id: 2,
      name: 'Mechanical'
    },
    {
      id: 3,
      name: 'Civil'
    },
    {
      id: 4,
      name: 'Biomedical'
    },
    {
      id: 5,
      name: 'Nuclear'
    },
    {
      id: 6,
      name: 'Industrial'
    },
    {
      id: 7,
      name: 'Chemical'
    },
    {
      id: 8,
      name:
        'Agricultural'
    },
    {
      id: 9,
      name: 'Electrical'
    }
  ];
  let [
    majorId,
    setMajorId
  ] = React.useState<
    React.Key | null
  >(null);

  return (
    <>
      <MyComboBox
        defaultItems={options}
        onSelectionChange={setMajorId}
      >
        {(item) => (
          <Item>
            {item.name}
          </Item>
        )}
      </MyComboBox>
      <p>
        Selected topic
        id: {majorId}
      </p>
    </>
  );
}

Allow custom values#

By default, ComboBox doesn't allow users to specify a value that doesn't exist in the list of options and will revert the input value to the current selected value on blur. By specifying allowsCustomValue, this behavior is suppressed and the user is free to enter any value within the field.

<MyComboBox label="Favorite Animal" allowsCustomValue>
  <Item id="red panda">Red Panda</Item>
  <Item id="cat">Cat</Item>
  <Item id="dog">Dog</Item>
  <Item id="aardvark">Aardvark</Item>
  <Item id="kangaroo">Kangaroo</Item>
  <Item id="snake">Snake</Item>
</MyComboBox>
<MyComboBox label="Favorite Animal" allowsCustomValue>
  <Item id="red panda">Red Panda</Item>
  <Item id="cat">Cat</Item>
  <Item id="dog">Dog</Item>
  <Item id="aardvark">Aardvark</Item>
  <Item id="kangaroo">Kangaroo</Item>
  <Item id="snake">Snake</Item>
</MyComboBox>
<MyComboBox
  label="Favorite Animal"
  allowsCustomValue
>
  <Item id="red panda">
    Red Panda
  </Item>
  <Item id="cat">
    Cat
  </Item>
  <Item id="dog">
    Dog
  </Item>
  <Item id="aardvark">
    Aardvark
  </Item>
  <Item id="kangaroo">
    Kangaroo
  </Item>
  <Item id="snake">
    Snake
  </Item>
</MyComboBox>

Sections#

ComboBox supports sections in order to group options. Sections can be used by wrapping groups of items in a Section element. A <Header> element may also be included to label the section.

Static items#

import {Section, Header} from 'react-aria-components';

<MyComboBox label="Preferred fruit or vegetable">
  <Section>
    <Header>Fruit</Header>
    <Item id="Apple">Apple</Item>
    <Item id="Banana">Banana</Item>
    <Item id="Orange">Orange</Item>
    <Item id="Honeydew">Honeydew</Item>
    <Item id="Grapes">Grapes</Item>
    <Item id="Watermelon">Watermelon</Item>
    <Item id="Cantaloupe">Cantaloupe</Item>
    <Item id="Pear">Pear</Item>
  </Section>
  <Section>
    <Header>Vegetable</Header>
    <Item id="Cabbage">Cabbage</Item>
    <Item id="Broccoli">Broccoli</Item>
    <Item id="Carrots">Carrots</Item>
    <Item id="Lettuce">Lettuce</Item>
    <Item id="Spinach">Spinach</Item>
    <Item id="Bok Choy">Bok Choy</Item>
    <Item id="Cauliflower">Cauliflower</Item>
    <Item id="Potatoes">Potatoes</Item>
  </Section>
</MyComboBox>
import {Section, Header} from 'react-aria-components';

<MyComboBox label="Preferred fruit or vegetable">
  <Section>
    <Header>Fruit</Header>
    <Item id="Apple">Apple</Item>
    <Item id="Banana">Banana</Item>
    <Item id="Orange">Orange</Item>
    <Item id="Honeydew">Honeydew</Item>
    <Item id="Grapes">Grapes</Item>
    <Item id="Watermelon">Watermelon</Item>
    <Item id="Cantaloupe">Cantaloupe</Item>
    <Item id="Pear">Pear</Item>
  </Section>
  <Section>
    <Header>Vegetable</Header>
    <Item id="Cabbage">Cabbage</Item>
    <Item id="Broccoli">Broccoli</Item>
    <Item id="Carrots">Carrots</Item>
    <Item id="Lettuce">Lettuce</Item>
    <Item id="Spinach">Spinach</Item>
    <Item id="Bok Choy">Bok Choy</Item>
    <Item id="Cauliflower">Cauliflower</Item>
    <Item id="Potatoes">Potatoes</Item>
  </Section>
</MyComboBox>
import {
  Header,
  Section
} from 'react-aria-components';

<MyComboBox label="Preferred fruit or vegetable">
  <Section>
    <Header>
      Fruit
    </Header>
    <Item id="Apple">
      Apple
    </Item>
    <Item id="Banana">
      Banana
    </Item>
    <Item id="Orange">
      Orange
    </Item>
    <Item id="Honeydew">
      Honeydew
    </Item>
    <Item id="Grapes">
      Grapes
    </Item>
    <Item id="Watermelon">
      Watermelon
    </Item>
    <Item id="Cantaloupe">
      Cantaloupe
    </Item>
    <Item id="Pear">
      Pear
    </Item>
  </Section>
  <Section>
    <Header>
      Vegetable
    </Header>
    <Item id="Cabbage">
      Cabbage
    </Item>
    <Item id="Broccoli">
      Broccoli
    </Item>
    <Item id="Carrots">
      Carrots
    </Item>
    <Item id="Lettuce">
      Lettuce
    </Item>
    <Item id="Spinach">
      Spinach
    </Item>
    <Item id="Bok Choy">
      Bok Choy
    </Item>
    <Item id="Cauliflower">
      Cauliflower
    </Item>
    <Item id="Potatoes">
      Potatoes
    </Item>
  </Section>
</MyComboBox>

Dynamic items#

Sections used with dynamic items are populated from a hierarchical data structure. Please note that Section takes an array of data using the items prop only. If the section also has a header, the Collection component can be used to render the child items.

import {Collection} from 'react-aria-components';

function Example() {
  let options = [
    {name: 'Fruit', children: [
      {name: 'Apple'},
      {name: 'Banana'},
      {name: 'Orange'},
      {name: 'Honeydew'},
      {name: 'Grapes'},
      {name: 'Watermelon'},
      {name: 'Cantaloupe'},
      {name: 'Pear'}
    ]},
    {name: 'Vegetable', children: [
      {name: 'Cabbage'},
      {name: 'Broccoli'},
      {name: 'Carrots'},
      {name: 'Lettuce'},
      {name: 'Spinach'},
      {name: 'Bok Choy'},
      {name: 'Cauliflower'},
      {name: 'Potatoes'}
    ]}
  ];

  return (
    <MyComboBox label="Preferred fruit or vegetable" defaultItems={options}>
      {section => (
        <Section id={section.name}>
          <Header>{section.name}</Header>
          <Collection items={section.children}>
            {item => <Item id={item.name}>{item.name}</Item>}
          </Collection>
        </Section>
      )}
    </MyComboBox>
  );
}
import {Collection} from 'react-aria-components';

function Example() {
  let options = [
    {
      name: 'Fruit',
      children: [
        { name: 'Apple' },
        { name: 'Banana' },
        { name: 'Orange' },
        { name: 'Honeydew' },
        { name: 'Grapes' },
        { name: 'Watermelon' },
        { name: 'Cantaloupe' },
        { name: 'Pear' }
      ]
    },
    {
      name: 'Vegetable',
      children: [
        { name: 'Cabbage' },
        { name: 'Broccoli' },
        { name: 'Carrots' },
        { name: 'Lettuce' },
        { name: 'Spinach' },
        { name: 'Bok Choy' },
        { name: 'Cauliflower' },
        { name: 'Potatoes' }
      ]
    }
  ];

  return (
    <MyComboBox
      label="Preferred fruit or vegetable"
      defaultItems={options}
    >
      {(section) => (
        <Section id={section.name}>
          <Header>{section.name}</Header>
          <Collection items={section.children}>
            {(item) => (
              <Item id={item.name}>{item.name}</Item>
            )}
          </Collection>
        </Section>
      )}
    </MyComboBox>
  );
}
import {Collection} from 'react-aria-components';

function Example() {
  let options = [
    {
      name: 'Fruit',
      children: [
        {
          name: 'Apple'
        },
        {
          name: 'Banana'
        },
        {
          name: 'Orange'
        },
        {
          name:
            'Honeydew'
        },
        {
          name: 'Grapes'
        },
        {
          name:
            'Watermelon'
        },
        {
          name:
            'Cantaloupe'
        },
        { name: 'Pear' }
      ]
    },
    {
      name: 'Vegetable',
      children: [
        {
          name: 'Cabbage'
        },
        {
          name:
            'Broccoli'
        },
        {
          name: 'Carrots'
        },
        {
          name: 'Lettuce'
        },
        {
          name: 'Spinach'
        },
        {
          name:
            'Bok Choy'
        },
        {
          name:
            'Cauliflower'
        },
        {
          name:
            'Potatoes'
        }
      ]
    }
  ];

  return (
    <MyComboBox
      label="Preferred fruit or vegetable"
      defaultItems={options}
    >
      {(section) => (
        <Section
          id={section
            .name}
        >
          <Header>
            {section
              .name}
          </Header>
          <Collection
            items={section
              .children}
          >
            {(item) => (
              <Item
                id={item
                  .name}
              >
                {item
                  .name}
              </Item>
            )}
          </Collection>
        </Section>
      )}
    </MyComboBox>
  );
}

Complex items#

Items within ComboBox also allow for additional content used to better communicate options. Icons and descriptions can be added to the children of Item as shown in the example below. If a description is added, the prop slot="description" must be used to distinguish the different <Text> elements.

import {Text} from 'react-aria-components';

<MyComboBox label="Select action">
  <Item textValue="Add to queue">
    <Text slot="label">Add to queue</Text>
    <Text slot="description">Add to current watch queue.</Text>
  </Item>
  <Item textValue="Add review">
    <Text slot="label">Add review</Text>
    <Text slot="description">Post a review for the episode.</Text>
  </Item>
  <Item textValue="Subscribe to series">
    <Text slot="label">Subscribe to series</Text>
    <Text slot="description">
      Add series to your subscription list and be notified when a new episode
      airs.
    </Text>
  </Item>
  <Item textValue="Report">
    <Text slot="label">Report</Text>
    <Text slot="description">Report an issue/violation.</Text>
  </Item>
</MyComboBox>
import {Text} from 'react-aria-components';

<MyComboBox label="Select action">
  <Item textValue="Add to queue">
    <Text slot="label">Add to queue</Text>
    <Text slot="description">
      Add to current watch queue.
    </Text>
  </Item>
  <Item textValue="Add review">
    <Text slot="label">Add review</Text>
    <Text slot="description">
      Post a review for the episode.
    </Text>
  </Item>
  <Item textValue="Subscribe to series">
    <Text slot="label">Subscribe to series</Text>
    <Text slot="description">
      Add series to your subscription list and be notified
      when a new episode airs.
    </Text>
  </Item>
  <Item textValue="Report">
    <Text slot="label">Report</Text>
    <Text slot="description">
      Report an issue/violation.
    </Text>
  </Item>
</MyComboBox>
import {Text} from 'react-aria-components';

<MyComboBox label="Select action">
  <Item textValue="Add to queue">
    <Text slot="label">
      Add to queue
    </Text>
    <Text slot="description">
      Add to current
      watch queue.
    </Text>
  </Item>
  <Item textValue="Add review">
    <Text slot="label">
      Add review
    </Text>
    <Text slot="description">
      Post a review for
      the episode.
    </Text>
  </Item>
  <Item textValue="Subscribe to series">
    <Text slot="label">
      Subscribe to
      series
    </Text>
    <Text slot="description">
      Add series to
      your subscription
      list and be
      notified when a
      new episode airs.
    </Text>
  </Item>
  <Item textValue="Report">
    <Text slot="label">
      Report
    </Text>
    <Text slot="description">
      Report an
      issue/violation.
    </Text>
  </Item>
</MyComboBox>

Custom filtering#

By default, ComboBox uses a "contains" function from useFilter to filter the list of options. This can be overridden using the defaultFilter prop, or by using the items prop to control the filtered list. When items is provided rather than defaultItems, ComboBox does no filtering of its own.

The following example makes the inputValue controlled, and updates the filtered list that is passed to the items prop when the input changes value.

import {useFilter} from '@react-aria/i18n';

function Example() {
  let options = [
    {id: 1, email: 'fake@email.com'},
    {id: 2, email: 'anotherfake@email.com'},
    {id: 3, email: 'bob@email.com'},
    {id: 4, email: 'joe@email.com'},
    {id: 5, email: 'yourEmail@email.com'},
    {id: 6, email: 'valid@email.com'},
    {id: 7, email: 'spam@email.com'},
    {id: 8, email: 'newsletter@email.com'},
    {id: 9, email: 'subscribe@email.com'}
  ];

  let {startsWith} = useFilter({sensitivity: 'base'});
  let [filterValue, setFilterValue] = React.useState('');
  let filteredItems = React.useMemo(
    () => options.filter((item) => startsWith(item.email, filterValue)),
    [options, filterValue]
  );

  return (
    <MyComboBox
      label="To:"
      items={filteredItems}
      inputValue={filterValue}
      onInputChange={setFilterValue}
      allowsCustomValue>
      {(item) => <Item>{item.email}</Item>}
    </MyComboBox>
  );
}
import {useFilter} from '@react-aria/i18n';

function Example() {
  let options = [
    { id: 1, email: 'fake@email.com' },
    { id: 2, email: 'anotherfake@email.com' },
    { id: 3, email: 'bob@email.com' },
    { id: 4, email: 'joe@email.com' },
    { id: 5, email: 'yourEmail@email.com' },
    { id: 6, email: 'valid@email.com' },
    { id: 7, email: 'spam@email.com' },
    { id: 8, email: 'newsletter@email.com' },
    { id: 9, email: 'subscribe@email.com' }
  ];

  let { startsWith } = useFilter({ sensitivity: 'base' });
  let [filterValue, setFilterValue] = React.useState('');
  let filteredItems = React.useMemo(
    () =>
      options.filter((item) =>
        startsWith(item.email, filterValue)
      ),
    [options, filterValue]
  );

  return (
    <MyComboBox
      label="To:"
      items={filteredItems}
      inputValue={filterValue}
      onInputChange={setFilterValue}
      allowsCustomValue
    >
      {(item) => <Item>{item.email}</Item>}
    </MyComboBox>
  );
}
import {useFilter} from '@react-aria/i18n';

function Example() {
  let options = [
    {
      id: 1,
      email:
        'fake@email.com'
    },
    {
      id: 2,
      email:
        'anotherfake@email.com'
    },
    {
      id: 3,
      email:
        'bob@email.com'
    },
    {
      id: 4,
      email:
        'joe@email.com'
    },
    {
      id: 5,
      email:
        'yourEmail@email.com'
    },
    {
      id: 6,
      email:
        'valid@email.com'
    },
    {
      id: 7,
      email:
        'spam@email.com'
    },
    {
      id: 8,
      email:
        'newsletter@email.com'
    },
    {
      id: 9,
      email:
        'subscribe@email.com'
    }
  ];

  let { startsWith } =
    useFilter({
      sensitivity: 'base'
    });
  let [
    filterValue,
    setFilterValue
  ] = React.useState('');
  let filteredItems =
    React.useMemo(
      () =>
        options.filter((
          item
        ) =>
          startsWith(
            item.email,
            filterValue
          )
        ),
      [
        options,
        filterValue
      ]
    );

  return (
    <MyComboBox
      label="To:"
      items={filteredItems}
      inputValue={filterValue}
      onInputChange={setFilterValue}
      allowsCustomValue
    >
      {(item) => (
        <Item>
          {item.email}
        </Item>
      )}
    </MyComboBox>
  );
}

Fully controlled#

The following example shows how you would create a controlled ComboBox, controlling everything from the selected value (selectedKey) to the combobox options (items). By passing in inputValue, selectedKey, and items to the ComboBox you can control exactly what your ComboBox should display. For example, note that the item filtering for the controlled ComboBox below now follows a "starts with" filter strategy, accomplished by controlling the exact set of items available to the ComboBox whenever the input value updates.

It is important to note that you don't have to control every single aspect of a ComboBox. If you decide to only control a single property of the ComboBox, be sure to provide the change handler for that prop as well e.g. controlling selectedKey would require onSelectionChange to be passed to ComboBox as well.

function ControlledComboBox() {
  let optionList = [
    { name: 'Red Panda', id: '1' },
    { name: 'Cat', id: '2' },
    { name: 'Dog', id: '3' },
    { name: 'Aardvark', id: '4' },
    { name: 'Kangaroo', id: '5' },
    { name: 'Snake', id: '6' }
  ];

  // Store ComboBox input value, selected option, open state, and items
  // in a state tracker
  let [fieldState, setFieldState] = React.useState({
    selectedKey: '' as React.Key,
    inputValue: '',
    items: optionList
  });

  // Implement custom filtering logic and control what items are
  // available to the ComboBox.
  let { startsWith } = useFilter({ sensitivity: 'base' });

  // Specify how each of the ComboBox values should change when an
  // option is selected from the list box
  let onSelectionChange = (key: React.Key) => {
    setFieldState((prevState) => {
      let selectedItem = prevState.items.find((option) => option.id === key);
      return ({
        inputValue: selectedItem?.name ?? '',
        selectedKey: key,
        items: optionList.filter((item) =>
          startsWith(item.name, selectedItem?.name ?? '')
        )
      });
    });
  };

  // Specify how each of the ComboBox values should change when the input
  // field is altered by the user
  let onInputChange = (value: string) => {
    setFieldState((prevState) => ({
      inputValue: value,
      selectedKey: value === '' ? '' : prevState.selectedKey,
      items: optionList.filter((item) => startsWith(item.name, value))
    }));
  };

  // Show entire list if user opens the menu manually
  let onOpenChange = (isOpen: boolean, menuTrigger?: string) => {
    if (menuTrigger === 'manual' && isOpen) {
      setFieldState((prevState) => ({
        inputValue: prevState.inputValue,
        selectedKey: prevState.selectedKey,
        items: optionList
      }));
    }
  };

  // Pass each controlled prop to ComboBox along with their
  // change handlers
  return (
    <MyComboBox
      label="Favorite Animal"
      items={fieldState.items}
      selectedKey={fieldState.selectedKey}
      inputValue={fieldState.inputValue}
      onOpenChange={onOpenChange}
      onSelectionChange={onSelectionChange}
      onInputChange={onInputChange}
    >
      {(item) => <Item>{item.name}</Item>}
    </MyComboBox>
  );
}

<ControlledComboBox />
function ControlledComboBox() {
  let optionList = [
    { name: 'Red Panda', id: '1' },
    { name: 'Cat', id: '2' },
    { name: 'Dog', id: '3' },
    { name: 'Aardvark', id: '4' },
    { name: 'Kangaroo', id: '5' },
    { name: 'Snake', id: '6' }
  ];

  // Store ComboBox input value, selected option, open state, and items
  // in a state tracker
  let [fieldState, setFieldState] = React.useState({
    selectedKey: '' as React.Key,
    inputValue: '',
    items: optionList
  });

  // Implement custom filtering logic and control what items are
  // available to the ComboBox.
  let { startsWith } = useFilter({ sensitivity: 'base' });

  // Specify how each of the ComboBox values should change when an
  // option is selected from the list box
  let onSelectionChange = (key: React.Key) => {
    setFieldState((prevState) => {
      let selectedItem = prevState.items.find((option) =>
        option.id === key
      );
      return ({
        inputValue: selectedItem?.name ?? '',
        selectedKey: key,
        items: optionList.filter((item) =>
          startsWith(item.name, selectedItem?.name ?? '')
        )
      });
    });
  };

  // Specify how each of the ComboBox values should change when the input
  // field is altered by the user
  let onInputChange = (value: string) => {
    setFieldState((prevState) => ({
      inputValue: value,
      selectedKey: value === ''
        ? ''
        : prevState.selectedKey,
      items: optionList.filter((item) =>
        startsWith(item.name, value)
      )
    }));
  };

  // Show entire list if user opens the menu manually
  let onOpenChange = (
    isOpen: boolean,
    menuTrigger?: string
  ) => {
    if (menuTrigger === 'manual' && isOpen) {
      setFieldState((prevState) => ({
        inputValue: prevState.inputValue,
        selectedKey: prevState.selectedKey,
        items: optionList
      }));
    }
  };

  // Pass each controlled prop to ComboBox along with their
  // change handlers
  return (
    <MyComboBox
      label="Favorite Animal"
      items={fieldState.items}
      selectedKey={fieldState.selectedKey}
      inputValue={fieldState.inputValue}
      onOpenChange={onOpenChange}
      onSelectionChange={onSelectionChange}
      onInputChange={onInputChange}
    >
      {(item) => <Item>{item.name}</Item>}
    </MyComboBox>
  );
}

<ControlledComboBox />
function ControlledComboBox() {
  let optionList = [
    {
      name: 'Red Panda',
      id: '1'
    },
    {
      name: 'Cat',
      id: '2'
    },
    {
      name: 'Dog',
      id: '3'
    },
    {
      name: 'Aardvark',
      id: '4'
    },
    {
      name: 'Kangaroo',
      id: '5'
    },
    {
      name: 'Snake',
      id: '6'
    }
  ];

  // Store ComboBox input value, selected option, open state, and items
  // in a state tracker
  let [
    fieldState,
    setFieldState
  ] = React.useState({
    selectedKey:
      '' as React.Key,
    inputValue: '',
    items: optionList
  });

  // Implement custom filtering logic and control what items are
  // available to the ComboBox.
  let { startsWith } =
    useFilter({
      sensitivity: 'base'
    });

  // Specify how each of the ComboBox values should change when an
  // option is selected from the list box
  let onSelectionChange =
    (key: React.Key) => {
      setFieldState(
        (prevState) => {
          let selectedItem =
            prevState
              .items
              .find(
                (option) =>
                  option
                    .id ===
                    key
              );
          return ({
            inputValue:
              selectedItem
                ?.name ??
                '',
            selectedKey:
              key,
            items:
              optionList
                .filter(
                  (item) =>
                    startsWith(
                      item
                        .name,
                      selectedItem
                        ?.name ??
                        ''
                    )
                )
          });
        }
      );
    };

  // Specify how each of the ComboBox values should change when the input
  // field is altered by the user
  let onInputChange = (
    value: string
  ) => {
    setFieldState(
      (prevState) => ({
        inputValue:
          value,
        selectedKey:
          value === ''
            ? ''
            : prevState
              .selectedKey,
        items: optionList
          .filter(
            (item) =>
              startsWith(
                item
                  .name,
                value
              )
          )
      })
    );
  };

  // Show entire list if user opens the menu manually
  let onOpenChange = (
    isOpen: boolean,
    menuTrigger?: string
  ) => {
    if (
      menuTrigger ===
        'manual' &&
      isOpen
    ) {
      setFieldState(
        (prevState) => ({
          inputValue:
            prevState
              .inputValue,
          selectedKey:
            prevState
              .selectedKey,
          items:
            optionList
        })
      );
    }
  };

  // Pass each controlled prop to ComboBox along with their
  // change handlers
  return (
    <MyComboBox
      label="Favorite Animal"
      items={fieldState
        .items}
      selectedKey={fieldState
        .selectedKey}
      inputValue={fieldState
        .inputValue}
      onOpenChange={onOpenChange}
      onSelectionChange={onSelectionChange}
      onInputChange={onInputChange}
    >
      {(item) => (
        <Item>
          {item.name}
        </Item>
      )}
    </MyComboBox>
  );
}

<ControlledComboBox />

ComboBox supports three different menuTrigger prop values:

  • input (default): ComboBox menu opens when the user edits the input text.
  • focus: ComboBox menu opens when the user focuses the ComboBox input.
  • manual: ComboBox menu only opens when the user presses the trigger button or uses the arrow keys.

The example below has menuTrigger set to focus.

<MyComboBox label="Favorite Animal" menuTrigger="focus">
  <Item id="red panda">Red Panda</Item>
  <Item id="cat">Cat</Item>
  <Item id="dog">Dog</Item>
  <Item id="aardvark">Aardvark</Item>
  <Item id="kangaroo">Kangaroo</Item>
  <Item id="snake">Snake</Item>
</MyComboBox>
<MyComboBox label="Favorite Animal" menuTrigger="focus">
  <Item id="red panda">Red Panda</Item>
  <Item id="cat">Cat</Item>
  <Item id="dog">Dog</Item>
  <Item id="aardvark">Aardvark</Item>
  <Item id="kangaroo">Kangaroo</Item>
  <Item id="snake">Snake</Item>
</MyComboBox>
<MyComboBox
  label="Favorite Animal"
  menuTrigger="focus"
>
  <Item id="red panda">
    Red Panda
  </Item>
  <Item id="cat">
    Cat
  </Item>
  <Item id="dog">
    Dog
  </Item>
  <Item id="aardvark">
    Aardvark
  </Item>
  <Item id="kangaroo">
    Kangaroo
  </Item>
  <Item id="snake">
    Snake
  </Item>
</MyComboBox>

Disabled options#

You can disable specific options by providing an array of keys to ComboBox via the disabledKeys prop. This will prevent options with matching keys from being pressable and receiving keyboard focus as shown in the example below. Note that you are responsible for the styling of disabled options.

<MyComboBox label="Favorite Animal" disabledKeys={['cat', 'kangaroo']}>
  <Item id="red panda">Red Panda</Item>
  <Item id="cat">Cat</Item>
  <Item id="dog">Dog</Item>
  <Item id="aardvark">Aardvark</Item>
  <Item id="kangaroo">Kangaroo</Item>
  <Item id="snake">Snake</Item>
</MyComboBox>
<MyComboBox
  label="Favorite Animal"
  disabledKeys={['cat', 'kangaroo']}
>
  <Item id="red panda">Red Panda</Item>
  <Item id="cat">Cat</Item>
  <Item id="dog">Dog</Item>
  <Item id="aardvark">Aardvark</Item>
  <Item id="kangaroo">Kangaroo</Item>
  <Item id="snake">Snake</Item>
</MyComboBox>
<MyComboBox
  label="Favorite Animal"
  disabledKeys={[
    'cat',
    'kangaroo'
  ]}
>
  <Item id="red panda">
    Red Panda
  </Item>
  <Item id="cat">
    Cat
  </Item>
  <Item id="dog">
    Dog
  </Item>
  <Item id="aardvark">
    Aardvark
  </Item>
  <Item id="kangaroo">
    Kangaroo
  </Item>
  <Item id="snake">
    Snake
  </Item>
</MyComboBox>

Asynchronous loading#

This example uses the useAsyncList hook to handle asynchronous loading and filtering of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. See this CodeSandbox for an example of a ComboBox supporting those features.

import {useAsyncList} from '@react-stately/data';

interface Character {
  name: string
}

function AsyncLoadingExample() {
  let list = useAsyncList<Character>({
    async load({signal, filterText}) {
      let res = await fetch(
        `https://swapi.py4e.com/api/people/?search=${filterText}`,
        {signal}
      );
      let json = await res.json();

      return {
        items: json.results
      };
    }
  });

  return (
    <MyComboBox
      label="Star Wars Character Lookup"
      items={list.items}
      inputValue={list.filterText}
      onInputChange={list.setFilterText}>
      {(item) => <Item id={item.name}>{item.name}</Item>}
    </MyComboBox>
  );
}
import {useAsyncList} from '@react-stately/data';

interface Character {
  name: string;
}

function AsyncLoadingExample() {
  let list = useAsyncList<Character>({
    async load({ signal, filterText }) {
      let res = await fetch(
        `https://swapi.py4e.com/api/people/?search=${filterText}`,
        { signal }
      );
      let json = await res.json();

      return {
        items: json.results
      };
    }
  });

  return (
    <MyComboBox
      label="Star Wars Character Lookup"
      items={list.items}
      inputValue={list.filterText}
      onInputChange={list.setFilterText}
    >
      {(item) => <Item id={item.name}>{item.name}</Item>}
    </MyComboBox>
  );
}
import {useAsyncList} from '@react-stately/data';

interface Character {
  name: string;
}

function AsyncLoadingExample() {
  let list =
    useAsyncList<
      Character
    >({
      async load(
        {
          signal,
          filterText
        }
      ) {
        let res =
          await fetch(
            `https://swapi.py4e.com/api/people/?search=${filterText}`,
            { signal }
          );
        let json =
          await res
            .json();

        return {
          items:
            json.results
        };
      }
    });

  return (
    <MyComboBox
      label="Star Wars Character Lookup"
      items={list.items}
      inputValue={list
        .filterText}
      onInputChange={list
        .setFilterText}
    >
      {(item) => (
        <Item
          id={item.name}
        >
          {item.name}
        </Item>
      )}
    </MyComboBox>
  );
}

Help text#

The description slot can be used to associate additional help text with a ComboBox. Additionally, the errorMessage slot can be used to help the user fix a validation error. It should be combined with the validationState prop to semantically mark the ComboBox as invalid for assistive technologies.

function Example() {
  let [animalId, setAnimalId] = React.useState<React.Key | null>(null);
  let options = [
    { id: 1, name: 'Aardvark' },
    { id: 2, name: 'Cat' },
    { id: 3, name: 'Dog' },
    { id: 4, name: 'Kangaroo' },
    { id: 5, name: 'Koala' },
    { id: 6, name: 'Penguin' },
    { id: 7, name: 'Snake' },
    { id: 8, name: 'Turtle' },
    { id: 9, name: 'Wombat' }
  ];
  let isValid = React.useMemo(() => animalId !== 2 && animalId !== 7, [
    animalId
  ]);

  return (
    <MyComboBox
      validationState={!animalId ? undefined : isValid ? 'valid' : 'invalid'}
      label="Favorite animal"
      description={isValid
        ? 'Pick your favorite animal, you will be judged.'
        : null}
      errorMessage={isValid
        ? null
        : animalId === 2
        ? 'The author of this example is a dog person.'
        : "Oh no it's a snake! Choose anything else."}
      items={options}
      selectedKey={animalId}
      onSelectionChange={(selected) => setAnimalId(selected)}
    >
      {(item) => <Item>{item.name}</Item>}
    </MyComboBox>
  );
}
function Example() {
  let [animalId, setAnimalId] = React.useState<
    React.Key | null
  >(null);
  let options = [
    { id: 1, name: 'Aardvark' },
    { id: 2, name: 'Cat' },
    { id: 3, name: 'Dog' },
    { id: 4, name: 'Kangaroo' },
    { id: 5, name: 'Koala' },
    { id: 6, name: 'Penguin' },
    { id: 7, name: 'Snake' },
    { id: 8, name: 'Turtle' },
    { id: 9, name: 'Wombat' }
  ];
  let isValid = React.useMemo(
    () => animalId !== 2 && animalId !== 7,
    [
      animalId
    ]
  );

  return (
    <MyComboBox
      validationState={!animalId
        ? undefined
        : isValid
        ? 'valid'
        : 'invalid'}
      label="Favorite animal"
      description={isValid
        ? 'Pick your favorite animal, you will be judged.'
        : null}
      errorMessage={isValid
        ? null
        : animalId === 2
        ? 'The author of this example is a dog person.'
        : "Oh no it's a snake! Choose anything else."}
      items={options}
      selectedKey={animalId}
      onSelectionChange={(selected) =>
        setAnimalId(selected)}
    >
      {(item) => <Item>{item.name}</Item>}
    </MyComboBox>
  );
}
function Example() {
  let [
    animalId,
    setAnimalId
  ] = React.useState<
    React.Key | null
  >(null);
  let options = [
    {
      id: 1,
      name: 'Aardvark'
    },
    {
      id: 2,
      name: 'Cat'
    },
    {
      id: 3,
      name: 'Dog'
    },
    {
      id: 4,
      name: 'Kangaroo'
    },
    {
      id: 5,
      name: 'Koala'
    },
    {
      id: 6,
      name: 'Penguin'
    },
    {
      id: 7,
      name: 'Snake'
    },
    {
      id: 8,
      name: 'Turtle'
    },
    {
      id: 9,
      name: 'Wombat'
    }
  ];
  let isValid = React
    .useMemo(
      () =>
        animalId !== 2 &&
        animalId !== 7,
      [
        animalId
      ]
    );

  return (
    <MyComboBox
      validationState={!animalId
        ? undefined
        : isValid
        ? 'valid'
        : 'invalid'}
      label="Favorite animal"
      description={isValid
        ? 'Pick your favorite animal, you will be judged.'
        : null}
      errorMessage={isValid
        ? null
        : animalId === 2
        ? 'The author of this example is a dog person.'
        : "Oh no it's a snake! Choose anything else."}
      items={options}
      selectedKey={animalId}
      onSelectionChange={(
        selected
      ) =>
        setAnimalId(
          selected
        )}
    >
      {(item) => (
        <Item>
          {item.name}
        </Item>
      )}
    </MyComboBox>
  );
}

Advanced customization#


Composition#

If you need to customize one of the components within a ComboBox, such as Input or ListBox, in many cases you can create a wrapper component. This lets you customize the props passed to the component.

function MyListBox(props) {
  return <ListBox {...props} className="my-listbox" />
}
function MyListBox(props) {
  return <ListBox {...props} className="my-listbox" />
}
function MyListBox(
  props
) {
  return (
    <ListBox
      {...props}
      className="my-listbox"
    />
  );
}

Hooks#

If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. ComboBox sends props to its child elements via public React contexts for each component. You can use this context to implement replacements for any component, using hooks from react-aria. This allows you to replace only the components you need to customize, and keep using the others.

ComboBox uses the following hooks. See the linked documentation for more details.

To replace a component used within a ComboBox, create your own component and use the useContextProps hook to merge the local props and ref with the ones sent via context by ComboBox. This example shows how you could implement a custom ListBox component that works with ComboBox.

import {ListBoxContext, useContextProps} from 'react-aria-components';
import {useListBox} from 'react-aria';

function MyListBox(props) {
  // Merge local props and ref with props from context.
  let ref = React.useRef();
  [props, ref] = useContextProps(props, ref, ListBoxContext);

  // Get state sent from ComboBox via context, and call useListBox.
  let {state} = React.useContext(ListBoxContext);
  let {listBoxProps} = useListBox(props, state, ref);

  // Render stuff
  // ...
}
import {
  ListBoxContext,
  useContextProps
} from 'react-aria-components';
import {useListBox} from 'react-aria';

function MyListBox(props) {
  // Merge local props and ref with props from context.
  let ref = React.useRef();
  [props, ref] = useContextProps(
    props,
    ref,
    ListBoxContext
  );

  // Get state sent from ComboBox via context, and call useListBox.
  let { state } = React.useContext(ListBoxContext);
  let { listBoxProps } = useListBox(props, state, ref);

  // Render stuff
  // ...
}
import {
  ListBoxContext,
  useContextProps
} from 'react-aria-components';
import {useListBox} from 'react-aria';

function MyListBox(
  props
) {
  // Merge local props and ref with props from context.
  let ref = React
    .useRef();
  [props, ref] =
    useContextProps(
      props,
      ref,
      ListBoxContext
    );

  // Get state sent from ComboBox via context, and call useListBox.
  let { state } = React
    .useContext(
      ListBoxContext
    );
  let { listBoxProps } =
    useListBox(
      props,
      state,
      ref
    );

  // Render stuff
  // ...
}

This also works the other way. If you need to customize ComboBox itself, but want to reuse the components it contains, you can do so by providing the necessary contexts. The Provider component is an easier way to send multiple contexts at once.

import {ButtonContext, InputContext, LabelContext, ListBoxContext, PopoverContext, useComboBox} from 'react-aria';
import {Provider} from 'react-aria-components';

function MyComboBox(props) {
  // ...
  let buttonRef = useRef(null);
  let inputRef = useRef(null);
  let listBoxRef = useRef(null);
  let popoverRef = useRef(null);
  let {
    buttonProps,
    inputProps,
    listBoxProps,
    labelProps
  } = useComboBox({/* ... */});

  return (
    <Provider
      values={[
        [LabelContext, { ...labelProps, ref: labelRef }],
        [ButtonContext, { ...buttonProps, ref: buttonRef }],
        [InputContext, { ...inputProps, ref: inputRef }],
        [PopoverContext, { ref: popoverRef }],
        [ListBoxContext, { ...listBoxProps, ref: listBoxRef }]
      ]}
    >
      {props.children}
    </Provider>
  );
}
import {
  ButtonContext,
  InputContext,
  LabelContext,
  ListBoxContext,
  PopoverContext,
  useComboBox
} from 'react-aria';
import {Provider} from 'react-aria-components';

function MyComboBox(props) {
  // ...
  let buttonRef = useRef(null);
  let inputRef = useRef(null);
  let listBoxRef = useRef(null);
  let popoverRef = useRef(null);
  let {
    buttonProps,
    inputProps,
    listBoxProps,
    labelProps
  } = useComboBox({/* ... */});

  return (
    <Provider
      values={[
        [LabelContext, { ...labelProps, ref: labelRef }],
        [ButtonContext, { ...buttonProps, ref: buttonRef }],
        [InputContext, { ...inputProps, ref: inputRef }],
        [PopoverContext, { ref: popoverRef }],
        [ListBoxContext, {
          ...listBoxProps,
          ref: listBoxRef
        }]
      ]}
    >
      {props.children}
    </Provider>
  );
}
import {
  ButtonContext,
  InputContext,
  LabelContext,
  ListBoxContext,
  PopoverContext,
  useComboBox
} from 'react-aria';
import {Provider} from 'react-aria-components';

function MyComboBox(
  props
) {
  // ...
  let buttonRef = useRef(
    null
  );
  let inputRef = useRef(
    null
  );
  let listBoxRef =
    useRef(null);
  let popoverRef =
    useRef(null);
  let {
    buttonProps,
    inputProps,
    listBoxProps,
    labelProps
  } = useComboBox({
    /* ... */
  });

  return (
    <Provider
      values={[
        [LabelContext, {
          ...labelProps,
          ref: labelRef
        }],
        [ButtonContext, {
          ...buttonProps,
          ref: buttonRef
        }],
        [InputContext, {
          ...inputProps,
          ref: inputRef
        }],
        [
          PopoverContext,
          {
            ref:
              popoverRef
          }
        ],
        [
          ListBoxContext,
          {
            ...listBoxProps,
            ref:
              listBoxRef
          }
        ]
      ]}
    >
      {props.children}
    </Provider>
  );
}