Skip to main contentSkip to docs navigation

A select-style component built with HTML — a toggle plus a .menu overlay — with JS handling open/close, filtering, selection, hidden-input submission, and keyboard navigation.

@layer components
Requires JavaScript
Depends onForm Input, Menu
Component tokens

Overview

A combobox is a select-style picker with one of two trigger styles:

  • Input trigger<input class="combobox-value"> inside the toggle. Typing filters the list inline. The same input also displays the selected value when the menu is closed.
  • Button trigger<span class="combobox-value"> (or any non-input element) inside the toggle. The toggle is display-only, like a native <select>. Add a .combobox-search-input inside the menu when the list is long enough that filtering helps.

The toggle, items, and decoration are written in HTML. The JavaScript wires up open/close, item filtering, selection state, keyboard navigation, and an auto-created hidden <input> for form submission.

Basic usage

A combobox needs three things: a toggle marked with data-cx-toggle="combobox", a .combobox-value element inside it, and a sibling .menu whose buttons carry data-cx-value attributes.

Examples below focus on the combobox markup itself and omit visible labels for clarity. See In a form field for the canonical labelled pattern.

Input trigger

The toggle is a wrapper <div class="form-input combobox"> with a text input inside. Typing filters the menu items as the user types.

HTML
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="option">
  <input type="text" class="combobox-value" placeholder="Select an item…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="1">Option one</button>
  <button class="menu-item" type="button" role="option" data-cx-value="2">Option two</button>
  <button class="menu-item" type="button" role="option" data-cx-value="3">Option three</button>
</div>

Button trigger

The toggle is itself a <button class="form-input combobox">. The user cannot type into the toggle, so the menu opens like a native <select>. Use data-cx-placeholder to set the text shown when nothing is selected — the JS restores it to the .combobox-value element after the selection is cleared.

HTML
<button class="form-input combobox"
        type="button"
        aria-label="Select an item"
        data-cx-toggle="combobox"
        data-cx-name="option"
        data-cx-placeholder="Select an item…">
  <span class="combobox-value"></span>
</button>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="1">Option one</button>
  <button class="menu-item" type="button" role="option" data-cx-value="2">Option two</button>
  <button class="menu-item" type="button" role="option" data-cx-value="3">Option three</button>
</div>

Pre-selected value

Mark the initially selected menu item with both the .selected class and aria-selected="true". On initialisation the JS reads .selected to set the toggle's displayed value and the hidden input. ARIA attributes on subsequent selections are managed automatically.

HTML
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="role">
  <input type="text" class="combobox-value" placeholder="Choose a role…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="admin">Admin</button>
  <button class="menu-item selected" type="button" role="option" aria-selected="true" data-cx-value="editor">Editor</button>
  <button class="menu-item" type="button" role="option" data-cx-value="viewer">Viewer</button>
</div>

Sizing

Add .small or .large to the toggle.

HTML
<div class="form-input small combobox" data-cx-toggle="combobox">
  <input type="text" class="combobox-value" placeholder="Small combobox…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="1">Option one</button>
  <button class="menu-item" type="button" role="option" data-cx-value="2">Option two</button>
</div>

<div class="form-input combobox" data-cx-toggle="combobox">
  <input type="text" class="combobox-value" placeholder="Default combobox…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="1">Option one</button>
  <button class="menu-item" type="button" role="option" data-cx-value="2">Option two</button>
</div>

<div class="form-input large combobox" data-cx-toggle="combobox">
  <input type="text" class="combobox-value" placeholder="Large combobox…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="1">Option one</button>
  <button class="menu-item" type="button" role="option" data-cx-value="2">Option two</button>
</div>

The menu floats independently of the toggle by default. To pin its width to the toggle's, wrap both in a .position-relative container and add w-100 to the menu.

HTML
<div class="position-relative">
  <div class="form-input combobox" data-cx-toggle="combobox" data-cx-name="option">
    <input type="text" class="combobox-value" placeholder="Select an item…" autocomplete="off">
  </div>
  <div class="menu w-100" role="listbox">
    <button class="menu-item" type="button" role="option" data-cx-value="1">Option one</button>
    <button class="menu-item" type="button" role="option" data-cx-value="2">Option two</button>
    <button class="menu-item" type="button" role="option" data-cx-value="3">Option three</button>
  </div>
</div>

Disabled

A combobox can be disabled as a whole, and individual menu items can be disabled too.

Whole combobox

Disable the toggle in one of three equivalent ways — the JS keeps all three sources in sync:

  • disabled attribute on the toggle (button trigger only)
  • disabled attribute on the embedded <input class="combobox-value"> (input trigger)
  • .disabled class on the toggle (either trigger)

The hidden input is also disabled, so the value is excluded from form submission.

HTML
<div class="form-input combobox" data-cx-toggle="combobox">
  <input type="text" class="combobox-value" placeholder="Unavailable…" autocomplete="off" disabled>
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="1">Option one</button>
  <button class="menu-item" type="button" role="option" data-cx-value="2">Option two</button>
</div>

<button class="form-input combobox"
        type="button"
        aria-label="Select an item"
        data-cx-toggle="combobox"
        data-cx-placeholder="Unavailable…"
        disabled>
  <span class="combobox-value"></span>
</button>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="1">Option one</button>
  <button class="menu-item" type="button" role="option" data-cx-value="2">Option two</button>
</div>

Individual items

Add disabled (or .disabled) to any .menu-item to make that option unselectable. The combobox itself stays usable.

HTML
<div class="form-input combobox" data-cx-toggle="combobox">
  <input type="text" class="combobox-value" placeholder="Select an option…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="1">Available option</button>
  <button class="menu-item" type="button" role="option" data-cx-value="2" disabled>Unavailable option</button>
  <button class="menu-item" type="button" role="option" data-cx-value="3">Another option</button>
</div>

To disable or re-enable a combobox at runtime, call the disable() and enable() methods.

A menu item is a button — anything that fits inside a button works, including SVG icons, descriptions, badges, images, or any combination.

Leading icon

Add .menu-item-icon to an <svg> at the start of the item. Mark it aria-hidden="true" so it doesn't duplicate the item's text for screen readers.

HTML
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="language">
  <input type="text" class="combobox-value" placeholder="Choose a language…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="html">
    <svg class="icon menu-item-icon" aria-hidden="true"><use href="#file-outline"></use></svg>
    <span>HTML</span>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="css">
    <svg class="icon menu-item-icon" aria-hidden="true"><use href="#file-outline"></use></svg>
    <span>CSS</span>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="js">
    <svg class="icon menu-item-icon" aria-hidden="true"><use href="#file-outline"></use></svg>
    <span>JavaScript</span>
  </button>
</div>

Title with description

Wrap the title and description in .menu-item-content (a flex column) and use .menu-item-description for the secondary line. The JS uses the first <span> inside .menu-item-content as the selected-state display text — keep that span as the canonical label.

HTML
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="role">
  <input type="text" class="combobox-value" placeholder="Select a role…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="admin">
    <span class="menu-item-content">
      <span>Admin</span>
      <small class="menu-item-description">Full access to all resources</small>
    </span>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="editor">
    <span class="menu-item-content">
      <span>Editor</span>
      <small class="menu-item-description">Can edit and publish content</small>
    </span>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="viewer">
    <span class="menu-item-content">
      <span>Viewer</span>
      <small class="menu-item-description">Read-only access</small>
    </span>
  </button>
</div>

Selection indicator

Add .menu-item-check to an <svg> inside each item. It stays hidden until the item gains .selected. A <hr class="menu-divider"> separates groups.

HTML
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="status">
  <input type="text" class="combobox-value" placeholder="Choose a status…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item selected" type="button" role="option" aria-selected="true" data-cx-value="active">
    <span class="menu-item-content">
      <span>Active</span>
      <small class="menu-item-description">Visible to users</small>
    </span>
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="draft">
    <span class="menu-item-content">
      <span>Draft</span>
      <small class="menu-item-description">Not yet published</small>
    </span>
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
  <hr class="menu-divider">
  <button class="menu-item" type="button" role="option" data-cx-value="archived">
    <span>Archived</span>
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
</div>

Combined

A richer item combines all three.

HTML
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="priority">
  <input type="text" class="combobox-value" placeholder="Set priority…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <button class="menu-item" type="button" role="option" data-cx-value="low">
    <svg class="icon menu-item-icon icon-info" aria-hidden="true"><use href="#clock-solid"></use></svg>
    <span class="menu-item-content">
      <span>Low</span>
      <small class="menu-item-description">No rush</small>
    </span>
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="medium">
    <svg class="icon menu-item-icon icon-warning" aria-hidden="true"><use href="#play-circle-solid"></use></svg>
    <span class="menu-item-content">
      <span>Medium</span>
      <small class="menu-item-description">Normal priority</small>
    </span>
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="high">
    <svg class="icon menu-item-icon icon-danger" aria-hidden="true"><use href="#exclamation-circle-solid"></use></svg>
    <span class="menu-item-content">
      <span>High</span>
      <small class="menu-item-description">Needs attention</small>
    </span>
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
</div>

Grouped items

Use .menu-header for non-interactive labels that introduce groups of related items. Mark each header role="presentation" so screen readers skip it instead of announcing it as an option.

HTML
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="language">
  <input type="text" class="combobox-value" placeholder="Select a language…" autocomplete="off">
</div>
<div class="menu" role="listbox">
  <div class="menu-header" role="presentation">Frontend</div>
  <button class="menu-item" type="button" role="option" data-cx-value="html">HTML</button>
  <button class="menu-item" type="button" role="option" data-cx-value="css">CSS</button>
  <button class="menu-item" type="button" role="option" data-cx-value="js">JavaScript</button>
  <div class="menu-header" role="presentation">Backend</div>
  <button class="menu-item" type="button" role="option" data-cx-value="python">Python</button>
  <button class="menu-item" type="button" role="option" data-cx-value="ruby">Ruby</button>
  <button class="menu-item" type="button" role="option" data-cx-value="go">Go</button>
</div>

Multiple selection

Add data-cx-multiple="true" to the toggle. Clicks toggle items, the menu stays open until dismissed, and the toggle text becomes N selected when more than one item is picked. The hidden input's value is a comma-separated list of the selected data-cx-values. Mark the menu aria-multiselectable="true" so the listbox semantics match the behaviour.

HTML
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="skills"
     data-cx-multiple="true">
  <input type="text" class="combobox-value" placeholder="Select skills…" autocomplete="off">
</div>
<div class="menu" role="listbox" aria-multiselectable="true">
  <button class="menu-item" type="button" role="option" data-cx-value="html">
    HTML
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="css">
    CSS
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="js">
    JavaScript
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="python">
    Python
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
  <button class="menu-item" type="button" role="option" data-cx-value="ruby">
    Ruby
    <svg class="icon menu-item-check" aria-hidden="true"><use href="#check-solid"></use></svg>
  </button>
</div>

Filtering long lists

Filtering hides items whose visible text does not match the current query. The mechanism differs between trigger styles: input-trigger comboboxes filter inline as the user types, while button-trigger comboboxes use a dedicated search field inside the menu.

Inline filtering

For input-trigger comboboxes, filtering is built in. As text is typed into the .combobox-value input, items whose visible text does not match are hidden. No extra markup required.

Search field

For button-trigger comboboxes, add a .combobox-search-input inside the menu. When the menu opens, focus jumps to the search field. Wrap it in .combobox-search for the sticky styling shown below, and add a .combobox-no-results element for the empty state — the JS toggles its .d-none class based on visible item count.

HTML
<button class="form-input combobox"
        type="button"
        aria-label="Select a country"
        data-cx-toggle="combobox"
        data-cx-name="country"
        data-cx-placeholder="Select a country…">
  <span class="combobox-value"></span>
</button>
<div class="menu" role="listbox">
  <div class="combobox-search">
    <input type="text" class="form-input combobox-search-input small" placeholder="Search…" autocomplete="off" aria-label="Filter countries">
  </div>
  <button class="menu-item" type="button" role="option" data-cx-value="us">United States</button>
  <button class="menu-item" type="button" role="option" data-cx-value="uk">United Kingdom</button>
  <button class="menu-item" type="button" role="option" data-cx-value="ca">Canada</button>
  <button class="menu-item" type="button" role="option" data-cx-value="au">Australia</button>
  <button class="menu-item" type="button" role="option" data-cx-value="de">Germany</button>
  <button class="menu-item" type="button" role="option" data-cx-value="fr">France</button>
  <button class="menu-item" type="button" role="option" data-cx-value="jp">Japan</button>
  <button class="menu-item" type="button" role="option" data-cx-value="br">Brazil</button>
  <button class="menu-item" type="button" role="option" data-cx-value="in">India</button>
  <button class="menu-item" type="button" role="option" data-cx-value="mx">Mexico</button>
  <div class="combobox-no-results d-none">No results found</div>
</div>

Accent-insensitive matching

Enable searchNormalize (via data-cx-search-normalize="true" or the JS option) to strip diacritics before comparing — so typing "uber" matches "über". Applies to both inline filtering and the search field.

In a form field

Wrap the label, combobox, and any feedback in a form field for a labelled, accessible layout. This is the canonical pattern for production use: a visible <label for="…"> provides the accessible name and is clickable to focus the combobox, and the menu's aria-label mirrors that name so screen-reader announcements stay consistent between the toggle and the listbox.

The billing region.
HTML
<div class="form-field">
  <label for="comboFormField" class="form-label">Country</label>
  <div class="form-input combobox"
      data-cx-toggle="combobox"
      data-cx-name="country">
    <input type="text" id="comboFormField" class="combobox-value" placeholder="Pick a country…" autocomplete="off">
  </div>
  <div class="menu w-100" role="listbox" aria-label="Country">
    <button class="menu-item" type="button" role="option" data-cx-value="us">United States</button>
    <button class="menu-item" type="button" role="option" data-cx-value="uk">United Kingdom</button>
    <button class="menu-item" type="button" role="option" data-cx-value="ca">Canada</button>
  </div>
  <div class="form-help">The billing region.</div>
</div>

Form submission

When data-cx-name is present on the toggle, the JS inserts a hidden <input> immediately before the toggle and keeps its value in sync with the selection. Without data-cx-name, no hidden input is created and the combobox's value is not submitted.

HTML
<!-- Authored markup -->
<div class="form-input combobox"
     data-cx-toggle="combobox"
     data-cx-name="country">
  ...
</div>
<div class="menu">...</div>

<!-- What the JS inserts before the toggle on init -->
<input type="hidden" name="country" value="us">

For multi-select, the hidden input's value is a comma-separated list — "html,css,js". When the combobox is disabled, the hidden input also gets disabled, so the value is excluded from form submission.

Accessibility

Combobox accessibility has two layers: behaviour the JS handles, and markup that must be authored.

Automatic behaviour

The JS keeps the following ARIA state in sync without any extra markup:

  • aria-expanded is set on the toggle when the menu opens (true) and closes (false).
  • aria-selected is set on each .menu-item after a selection change — "true" on selected items, "false" on others.
  • The toggle's .disabled class, aria-disabled attribute, and the disabled attribute on the embedded input are kept in sync. The hidden input's disabled is mirrored so disabled comboboxes do not submit a value.

Required markup

The semantics that identify the combobox and its options to assistive tech must be authored — the JS does not add them:

  • Accessible name for the combobox. Either a visible <label for="…"> pointing to the input/button id, or aria-label/aria-labelledby on the toggle. The In a form field section shows the canonical labelled pattern.
  • role="listbox" on the .menu. When labelled, also add aria-label (or aria-labelledby) matching the toggle's accessible name. For multi-select, add aria-multiselectable="true".
  • role="option" on each .menu-item, and aria-selected="true" on any pre-selected items. The JS keeps aria-selected in sync after that.
  • role="presentation" on .menu-header so headers are skipped by screen readers.
  • aria-hidden="true" on decorative SVG icons inside items.

Keyboard

A combobox is fully operable from the keyboard. Behaviour depends on where focus currently is:

  • Toggle focused, menu closed: / opens the menu and focuses the first or last visible item. Enter opens the menu (button trigger only — on input trigger, Enter behaves natively).
  • Menu open: / moves between items (wraps at ends). Home / End jumps to the first or last. Enter selects the focused item. Esc closes the menu and returns focus to the toggle. Tab closes the menu and moves focus normally.
  • Search field focused: jumps to the first visible item. Esc closes the menu.

JavaScript

The combobox can be configured through HTML data attributes or driven imperatively via the JS API. Data attributes cover most cases; the API is useful for runtime control (open/close, disable/enable, dispose) and for cases where the config needs to be computed at runtime.

Data attributes

The following attributes are read on init. Attribute values are strings; the JS coerces them to the option's expected type.

AttributeOnDescription
data-cx-toggle="combobox"toggleInitialises the combobox.
data-cx-nametogglename of the auto-created hidden input. Omit to skip hidden-input creation.
data-cx-placeholdertoggleText shown in the toggle when nothing is selected (button trigger).
data-cx-multipletoggle"true" enables multi-select.
data-cx-search-normalizetoggle"true" strips diacritics before comparing during filtering.
data-cx-value.menu-itemThe value submitted, and the source for the toggle's display text. Items without this attribute are decorative and not selectable.

Options

Pass via data attributes or as a config object during programmatic init.

NameTypeDefaultDescription
multiplebooleanfalseEnable multi-select. Selections toggle and the menu stays open.
namestring | nullnullName for the auto-created hidden input.
placeholderstring''Placeholder for the toggle (button trigger).
searchNormalizebooleanfalseStrip diacritics when filtering.
placementstring'bottom-start'Floating UI placement for the menu.
offsetarray | string | function[0, 2][skidding, distance] offset from the toggle.
boundarystring | element'clippingParents'Overflow boundary for the menu.

Methods

Call methods on a combobox instance retrieved via Combobox.getInstance(toggleElement) or Combobox.getOrCreateInstance(toggleElement).

MethodDescription
showOpens the menu.
hideCloses the menu.
toggleOpens or closes based on current state.
disableDisables the combobox and closes the menu if open.
enableRe-enables a disabled combobox.
disposeDestroys the instance and removes the hidden input.
getInstanceStatic. Returns the combobox instance attached to a DOM element, or null.
getOrCreateInstanceStatic. Returns the instance or creates one.

Events

Events are dispatched on the toggle element.

EventDescription
show.cx.comboboxFires when show() is called, before the menu opens. Preventable via event.preventDefault().
shown.cx.comboboxFires after the menu is visible.
hide.cx.comboboxFires when hide() is called, before the menu closes. Preventable.
hidden.cx.comboboxFires after the menu is hidden.
change.cx.comboboxFires after a selection. event.detail contains value (string for single select, array of strings for multi) and item (the selected .menu-item).

CSS

The Combobox component can be customized through Sass variables at compile time.

Combobox styling is split between component-specific rules and the form-element foundations shared with .form-input. Check the overview page to see the CSS properties and Sass variables shared by all form elements.

Sass variables

The Combobox component uses Sass variables in scss/config/_defaults.scss to define its defaults; they are also exposed as CSS custom properties using the --cx- prefix for runtime override — $variable-name becomes --cx-variable-name. See the component anatomy page.

$combobox-placeholder-color:        $form-idle-fg-inactive;
$combobox-no-results-color:         var(--fg-subtle);
$combobox-menu-max-height:          320px;
$combobox-menu-overflow-y:          auto;
$combobox-search-padding:           .5rem;
$combobox-search-bg-color:          inherit;