Skip to content

HyCombobox ​

Usage examples on this page are written for Lit / plain HTML (<hy-combobox>). The same component ships as HyCombobox in @whitespaceux/harmony-react (native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.

A combobox — a filterable single-select that combines a text input with a listbox popup. Follows the WAI-ARIA APG combobox pattern: DOM focus stays on the input, option highlight moves via aria-activedescendant.

The listbox is positioned with Floating UI: offset + flip + shift keep the popup inside the viewport near the input, and the size middleware caps the listbox height to the available space so long lists remain scrollable instead of spilling off-screen. autoUpdate repositions on scroll/resize while the listbox is open.

Scope: single-select, client-side filtering, no creatable free text, no virtualization. Multi-select ships as a sibling hy-multi-combobox, which also offers creatable values (creatable).

Select-only mode (select-only). The same picker with the text input replaced by a button — the APG "select-only combobox" (the Fluent Combobox / Dropdown split as one component, two modes). Nothing is typed and nothing filters: the button shows the picked option (sample and label, or the placeholder), focus does not open the list, a click / Enter / Space / an arrow / Home / End do, Enter / Space / Tab commit the highlighted row, Escape closes, Alt+Up commits and closes, PageUp / PageDown move by ten, and typing jumps to the next option whose label starts with what was typed (a repeated letter cycles). It is the picker for a field that must look the same in every browser — where hy-select's native picker is the OS's (Firefox, touch) — and for a rich row that must be picked, never searched.

Options are passed as a property (.options = [...]). The value property is the currently committed option value (or null). Filtering is controlled internally via filterOption; return false for every option to disable client filtering and drive options server-side. Focus opens the list only while options is non-empty, so a list fed as the user types stays closed until the first keystroke; an arrow key or the chevron open it regardless.

Option fields. Besides value / label / disabled, an option can carry a sample (one leading visual — a color square, a glyph, an avatar or text in its own face; see PickerSample), a description (a subdued second line, exposed as aria-description on the row), a group (consecutive options sharing a group render under one non-selectable header; the combobox never re-sorts), a disabledReason (shown under a disabled option and folded into its aria-description) and a trailing text at the end of the row (a count, a hint). When the selected option has a sample, the closed field shows it before the text. A person's avatar sample takes presence, categoryGroup and identity as any hy-avatar does — pass the identity the rest of the app uses, so the person has one color everywhere; the status dot shows in the list only, and a status that matters goes into the description too, since the sample is hidden from assistive technology. Filtering matches on label only — widen it through filterOption. Rows are data, never markup: the combobox owns its rows so the activedescendant model stays intact and the shape server-renders.

Width. The field fills its container. A field narrower than that states how many characters it holds with chars (1–8, 10, 12, 16, 20, 24, 32, 48 — the characters of the typed text, or of the shown value in select-only mode); the component adds its padding, border, chevron and clear button, and the width follows the design system's font. A select-only picker over a short, fixed list takes fit-options instead: it is exactly as wide as its widest option, like a native select (every label is laid out invisibly in the trigger, so the width is right in the server render and never jumps when the value changes; ignored above 50 options). width sets a fixed CSS width instead and wins over both. None grows past the container.

Listbox width. The popup opens at least as wide as the field and grows to fit its longest row (a description, a trailing text), up to --hy-combobox-surface-width-md and never past the viewport; past the cap a label or description wraps. A field wider than the cap keeps its own width.

Accessible name. Provide one of: the label property (visible label above the control), aria-label on the host (when the surrounding context already names it), or aria-labelledby referencing another element's id. The dev-mode WCAG 1.3.1 warning fires only when none of the three is set; aria-label / aria-labelledby on the host are forwarded to the inner <input> so screen readers see them.

A field or a settings row? ​

A field has its label above it, or beside it with layout="inline" (its helper and messages then sit under the control). A settings row — a setting's name with a line of description at the start and the control at the row's end, as on a preferences page — is a row of your own: a flex row with justify-content: space-between, the name and description as your own text, and the control named with aria-labelledby pointing at the name, with no label, helper-text or layout. Rebuilding a drawn settings page, keep the control at the row's end.

When to use

  • Picking one value from a long list people know the name of: a country, a person, a time zone.
  • A list people search rather than scan.

Do

  • Size the field with chars for the longest expected value.

When not to use

  • Two to five options people should compare at a glance: hy-radio-group.
  • A short list people scan rather than search: hy-select, or hy-combobox select-only when the field must look the same in every browser.
  • More than one value, such as tags or recipients: hy-multi-combobox.
  • Any text people type, with no list to pick from: hy-text-input.
  • Options people cannot guess and need to browse: hy-combobox select-only or hy-select, so the whole list shows without typing.

Accessibility: your part ​

  • For a person option, put a status that matters, such as "Away", in the option's description. The avatar's presence dot is not announced.

Examples ​

html
<hy-combobox label="Country" placeholder="Search countries"></hy-combobox>
<script>
  const box = document.querySelector('hy-combobox');
  box.options = [
    { value: 'ch', label: 'Switzerland' },
    { value: 'fr', label: 'France' },
    { value: 'de', label: 'Germany' },
  ];
  box.addEventListener('change', (e) => console.log(e.detail.value));
</script>

Option fields — sample, description, group, disabledReason, trailing ​

js
box.options = [
  {
    value: 'brand',
    label: 'Brand',
    group: 'Background',
    sample: { kind: 'color', value: 'var(--hy-background-brand-solid-rest)' },
    description: 'Solid fill at rest',
    trailing: '12',
  },
  { value: 'ch', label: 'Switzerland', group: 'Country', sample: { kind: 'icon', name: 'flag' } },
  {
    value: 'ada',
    label: 'Ada Lovelace',
    group: 'People',
    sample: {
      kind: 'avatar',
      initials: 'AL',
      presence: 'away',
      categoryGroup: 'team',
      identity: 'user-ada',
    },
    description: 'Away · ada@example.com',
  },
  {
    value: 'inter',
    label: 'Inter',
    group: 'Fonts',
    sample: { kind: 'text', value: 'Aa', fontFamily: 'Inter' },
  },
  { value: 'legacy', label: 'Legacy', disabled: true, disabledReason: 'Already overridden' },
];

API ​

Properties ​

PropertyAttributeTypeDefaultDescription
options—ComboboxOption[][]Option list. Property-only — arrays are not reflectable.
valuevaluestring | nullnullCommitted value. null when nothing is selected.
labellabelstring''Visible label above the input.
helperTexthelper-textstring''Helper text below the input. Replaced by the error or warning message while one shows.
errorMessageerror-messagestring''Error message below the input (replaces the helper text). Setting it raises error.
warningMessagewarning-messagestring''Warning message below the input (replaces the helper text). Setting it raises warning.
placeholderplaceholderstring''Placeholder shown when the input is empty.
sizesize'small' | 'medium' | 'large''medium'Input size — matches sibling form controls.
layoutlayout'block' | 'inline''block'Row layout: block (default) stacks label above the combobox; inline puts label on the left and combobox on the right. Helper/error always sit under the control (col 2–end), never under the label. Set --hy-combobox-label-column-width on a wrapping container so stacked comboboxes align.
widthwidthstring | undefined—A fixed width for the field, as any CSS width value (e.g. '400px', '20rem') — for a size the layout dictates rather than the content. Never wider than the container. To size a field to its content, use chars. Mirrors hy-text-input.
charschars1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | 16 | 20 | 24 | 32 | 48undefinedHow many characters the field holds — the way to make a field narrower than its container. One of 1–8, 10, 12, 16, 20, 24, 32, 48; the width is that many 0 glyphs in the field's own font (the typed text, or the shown value in select-only mode), plus its padding, border, chevron and clear button, so it follows every design system. Never wider than the container. A value outside the set is ignored (dev warning). width wins when both are set.
fitOptionsfit-optionsbooleanfalseSelect-only: the field is exactly as wide as its widest option (and the placeholder), like a native select — the way to size a picker over a short, fixed list, where chars would leave room for zeros the labels never fill. Every label is laid out invisibly in the trigger, so the width is right in the server render, follows a web font as it loads, and never changes with the value. Ignored on an editable combobox (typed text: use chars) and above 50 options (dev warning). Wins over chars; width wins over it. Never wider than the container.
namenamestring''Form name — surfaces the selected value to FormData.
disableddisabledbooleanfalseDisables the control.
requiredrequiredbooleanfalseMarks the field as required for form validation.
errorerrorbooleanfalseError state: the value is not accepted. Paints the danger border and sets aria-invalid; without error-message the helper text stays. Visual and for screen readers only — validity comes from required.
warningwarningbooleanfalseWarning state: the value is accepted but worth a second look. Paints the warning border at rest and shows warning-message; never sets aria-invalid and never blocks submission. error wins when both are set.
clearableclearablebooleantrueWhether to show a clear button when a value is set. On by default; turn it off from markup with clearable="false" (a bare clearable attribute, or none at all, keeps it on).
clearLabelclear-labelstring'Clear selection'Accessible name of the clear button.
emptyTextempty-textstring'No results'Fallback text when no options match the current query.
openOptionsLabelopen-options-labelstring'Open options'Accessible name of the expand button while the popup is closed. Use for internationalization.
closeOptionsLabelclose-options-labelstring'Close options'Accessible name of the expand button while the popup is open. Use for internationalization.
requiredLabelrequired-labelstring'required'The word announced for the required indicator (*) after the label. Use for internationalization.
openopenbooleanfalseControlled open state. Leave unset for uncontrolled behavior.
autofocusautofocusbooleanfalseMarks this combobox as the auto-focus target. Read by hy-dialog (and other overlay containers) on show to choose initial focus. Also focuses on first connect when used outside an overlay, mirroring native HTML autofocus but routed through JS so SSR'd HTML doesn't pull focus during hydration.
autoselectautoselectbooleanfalseAfter auto-focus lands, selects the current value so the user can type-to-replace. Pairs with autofocus for rename-style dialogs where the existing value is shown for context but is meant to be overwritten.
filterOption—ComboboxFilterdefaultFilterCustom filter. Called for each option; return true to include. Default is a case-insensitive substring match on label.
selectOnlyselect-onlybooleanfalseSelect-only mode: a button trigger instead of the text input — no typing, no filtering, the APG select-only combobox keyboard contract (see the class notes). filterOption, autoselect and the input event are inert in this mode.
form—HTMLFormElement | null——
validity—ValidityState——
validationMessage—string——

Events ​

EventDetailDescription
change—Fires when the selected value commits. Detail: ComboboxChangeDetail.
input—Fires while the user types. Detail: ComboboxInputDetail.
show—Fires when the listbox opens (does NOT bubble). Detail: HyVisibilityDetail.
hide—Fires when the listbox closes (does NOT bubble). Detail: HyVisibilityDetail.

CSS Parts ​

PartDescription
baseOuter wrapper containing label, control, helper/error.
labelThe <label> element.
containerThe input + trailing icons container.
inputThe text <input>.
triggerThe <button> that replaces the input in select-only mode.
clear-buttonThe clear affordance (visible when clearable and a value is set).
expand-iconThe chevron indicating popup state.
listboxThe popup <ul role="listbox">.
optionAn individual <li role="option">.
sampleThe sample cell on an option row or in the closed field (hy-swatch, hy-icon, hy-avatar or a text span, by kind).
option-descriptionThe subdued second line of an option.
option-reasonThe disabled reason under a disabled option.
option-trailingThe trailing text at the end of an option.
group-headerA non-selectable section header (<li role="presentation">).
emptyThe no-results message container.
helper-textHelper text below the input.
error-messageError message below the input.
warning-messageWarning message below the input.
message-iconThe icon starting the error or warning message.

CSS Custom Properties ​

The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.

PropertyDefaultDescription
--hy-combobox-listbox-max-rows—Plain rows the popup shows before it scrolls (a number, not a length — the cap lands on a row edge).
--hy-combobox-gap-row-list-xs-blockvar(--hy-gap-row-list-xs-block)Gap between two option rows, so a hovered row never touches the selected one (default: the row-list xs gap, the sidebar's 4 px at standard).
--hy-combobox-listbox-max-height—Maximum height of the popup before scrolling; overrides the row count with a length. The runtime keeps the smaller of this and the viewport space (Floating UI's size middleware).
--hy-combobox-surface-width-mdvar(--hy-surface-width-md)Widest the popup grows to fit its rows. The popup is never narrower than the field (a wider field wins) and never wider than the viewport space.
--hy-combobox-label-column-width—Width of the label column in layout="inline" mode. No default — set on a wrapping container so stacked comboboxes align.
--hy-combobox-control-height-smvar(--hy-control-height-sm)Outer height for small size variant. Per-component override in the form-control height contract.
--hy-combobox-control-height-mdvar(--hy-control-height-md)Outer height for medium size variant.
--hy-combobox-control-height-lgvar(--hy-control-height-lg)Outer height for large size variant.
--hy-combobox-motion-transition-hover-durationvar(--hy-motion-transition-hover-duration)Transition duration for hover and focus state feedback
--hy-combobox-motion-transition-hover-easingvar(--hy-motion-transition-hover-easing)Transition easing for hover and focus state feedback
--hy-combobox-motion-transition-expand-durationvar(--hy-motion-transition-expand-duration)Transition duration for the listbox expand and collapse animation
--hy-combobox-motion-transition-expand-easingvar(--hy-motion-transition-expand-easing)Transition easing for the listbox expand and collapse animation
--hy-combobox-indicator-size-mdvar(--hy-indicator-size-md)Edge of the sample cell on option rows and in the closed field.
--hy-combobox-description-foreground-subtle-restvar(--hy-foreground-subtle-rest)Ink of the option description line.
--hy-combobox-group-foreground-subtle-restvar(--hy-foreground-subtle-rest)Ink of a group header.
--hy-combobox-border-warning-restvar(--hy-border-warning-rest)Border color in the warning state, at rest.
--hy-combobox-foreground-warning-restvar(--hy-foreground-warning-rest)Warning message text and icon color.
--hy-combobox-gap-layout-2xsvar(--hy-gap-layout-2xs)Gap between the message icon and its text.
--hy-combobox-group-label-text-transformnoneCase of the option group headers in the list (none unless set: shown as written; uppercase for capitals). Display only — the header text stays as written.

Methods ​

select() ​

Selects all text in the combobox's underlying input. A no-op in select-only mode, where there is no text to select.

Built with Lit. Documented with VitePress.