Components
Combobox
Combine text input and option list for filter + commit interactions.
1. Live Preview
vanilla live preview activates on the client; source HTML keeps a readable preview island and primitive contract.
vanilla combobox preview activates on the client.
2. Installation
Install adapter-specific dependencies first, then complete setup via the installation guide. installation docs
pnpm add @nake-ui/primitives @nake-ui/adapter-vanilla @nake-ui/theme-vanilla
3. Usage
Minimal runnable usage for the current adapter.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const binding = bindCombobox(nodes, items, options);
binding.destroy();
4. Examples
Multiple (chips)
Builds chips-style multiple selection on top of `bindCombobox` with custom selected-value state.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const selectedValues: string[] = [];
const binding = bindCombobox(
{ root, input, listbox, options },
registrations,
{
allowCustomValue: false,
onValueCommit(value, optionId) {
if (optionId === null) {
return;
}
const index = selectedValues.indexOf(value);
if (index >= 0) {
selectedValues.splice(index, 1);
} else {
selectedValues.push(value);
}
input.value = "";
binding.controller.setInputValue("");
binding.sync();
}
}
);
input.setAttribute("aria-label", "Search fruits");
Clear Button
A right-side clear button resets query and keeps combobox ready for the next search.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const binding = bindCombobox({ root, input, listbox, options }, registrations);
clearButton.addEventListener("click", () => {
input.value = "";
binding.controller.setInputValue("");
input.focus();
binding.controller.setOpen(false);
binding.sync();
});
clearButton.setAttribute("aria-label", "Clear selected option");
Groups
Organizes grouped options with native `role=group` and `role=separator`.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const binding = bindCombobox({ root, input, listbox, options }, registrations, {
onValueCommit() {
binding.controller.setOpen(false);
binding.sync();
}
});
trigger.setAttribute("aria-label", "Open options list");
Invalid
Marks invalid input state via `aria-invalid` and `aria-describedby`.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const helperId = "combobox-invalid-help";
const binding = bindCombobox({ root, input, listbox, options }, registrations);
input.setAttribute("aria-invalid", "true");
input.setAttribute("aria-describedby", helperId);
helper.textContent = "Please choose a valid fruit option.";
binding.sync();
Disabled
Disables input/trigger so the combobox stays non-interactive.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
// Disabled input/trigger keeps the combobox non-interactive.
input.disabled = true;
trigger.disabled = true;
input.placeholder = "Combobox is disabled";
const binding = bindCombobox({ root, input, listbox, options }, registrations);
binding.sync();
Custom Option
Renders rich option content (main text + description) while keeping committed values stable.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const registrations = optionsData.map((option) => ({
id: option.id,
label: option.label,
value: option.value
}));
const binding = bindCombobox({ root, input, listbox, options }, registrations);
input.placeholder = "Pick an item";
binding.sync();
Auto Highlight
With `autoHighlight`, filtering automatically highlights the first match.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const binding = bindCombobox(
{ root, input, listbox, options },
registrations,
{ autoHighlight: true }
);
input.placeholder = "Type to filter";
binding.sync();
Popup
Uses a button trigger and places the search input inside the popup panel.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const binding = bindCombobox({ root, input, listbox, options }, registrations, {
closeOnOutsidePointerDown: true
});
trigger.addEventListener("click", () => {
binding.controller.setOpen(!binding.controller.getOpen());
binding.sync();
});
trigger.textContent = "Choose item";
Input Group
Adds an input addon prefix without changing combobox semantics.
import "@nake-ui/theme-vanilla/styles.css";
import { bindCombobox } from "@nake-ui/adapter-vanilla";
const binding = bindCombobox({ root, input, listbox, options }, registrations);
addon.textContent = "SKU";
binding.sync();
5. RTL
The same structure running inside a `dir="rtl"` container.
vanilla combobox preview activates on the client.
6. Closure Coverage
Complete
Closure Score: 100% (19/19)
Required Checks
-
schemaRegistered -
specDocumented -
reactMapper -
siteExampleCoverage -
siteA11yCoverage -
siteVisible -
schemaValidatorCoverage -
schemaAriaCoverage -
specValidatorRulesSection -
specTestChecklistSection -
specSsrHydrationSection -
reactSsrHydrationCoverage -
reactRenderLevelSsrCoverage -
solidSsrCoverage -
coreImplemented -
primitiveUnitTest -
domContractSnapshot -
vanillaBinding -
adapterParitySnapshot
All required closure checks are currently satisfied.
7. Purpose / Non-goals
Combine text input and option list for filter + commit interactions.
- Advanced fuzzy ranking policy
- Async suggestion fetching policy in core
8. Anatomy & Slot
rootanchor(optional, recommended)inputtrigger(optional)positioner(optional)content(optional)empty(optional)listboxoption(repeatable)chips(optional, multiple mode)chip(optional, repeatable, insidechips)
9. DOM Contract
<div data-ui="combobox" data-slot="root">
<div data-slot="anchor">
<input
data-slot="input"
data-state="open"
role="combobox"
aria-controls="combo-list"
aria-expanded="true"
aria-activedescendant="combo-option-apple"
value="Apple"
/>
</div>
<div data-slot="positioner" data-placement="bottom-start" data-side="bottom">
<div id="combo-list" data-slot="listbox" data-state="open" role="listbox">
<button
id="combo-option-apple"
data-slot="option"
data-selected="true"
data-highlighted="true"
role="option"
aria-selected="true"
>
Apple
</button>
<button id="combo-option-banana" data-slot="option" role="option" aria-selected="false">
Banana
</button>
</div>
</div>
</div>
10. State & Events
State Model
- input value controlled/uncontrolled
- open controlled/uncontrolled
- highlighted option id
- selected/committed value
- committed value is preserved across close, outside dismiss, and reopen
- reopen restores selected option highlight when no explicit highlight is active
Event Model
inputupdates query, recomputes visible options, and opens listboxmousedownon input opens listbox without mutating query- outside
pointerdowncloses listbox without clearing committed selection mouseenteron option updates highlighted optionmousedownon option commits the option valueEntercommits highlighted option; when no highlight andallowCustomValue=true, commits current input valueEscapecloses listbox without committingArrowUp/ArrowDown/Home/Endnavigate highlight through enabled options- in
multiplemode,Entertoggles highlighted value and keeps listbox open for continued selection - in
multiplemode,Backspaceremoves the last selected chip only when input is empty and IME composition is inactive
11. Keyboard & Focus
- focus remains on input during open, navigation, and commit flows
aria-activedescendantpoints to the highlighted option while listbox is open- default open state starts with no highlighted option
- if a value is already selected when reopening, the selected option is reflected as highlighted
- with
autoHighlight=trueand non-empty query, first visible enabled option becomes highlighted ArrowUp/ArrowDown/Home/Endmove highlight without moving DOM focus away from inputEntercommits highlighted option (or custom value when allowed);Escapecloses listbox- in
multiplemode,Backspacefrom an empty input removes the latest selected chip
12. ARIA
- input:
role=combobox,aria-controls,aria-expanded,aria-autocomplete=list - input: include
aria-activedescendantonly when an option is currently highlighted - listbox:
role=listbox; includearia-multiselectable=trueinmultiplemode - option:
role=option,aria-selected,aria-disabled(when disabled) - trigger (if rendered):
aria-haspopup=listbox,aria-expanded,aria-controls - invalid state should be reflected on input via
aria-invalid - combobox input must have an accessible name (
<label for>,aria-label, oraria-labelledby)
13. Validator Rules
-
structure.missing-required-slot -
structure.invalid-slot-placement -
aria.missing-label -
aria.broken-controls-linkage -
aria.invalid-activedescendant-target -
state.controlled-uncontrolled-conflict -
state.invalid-default-value -
keyboard.missing-required-keymap -
keyboard.unhandled-escape -
keyboard.unhandled-typeahead -
dom.missing-public-data-attr
14. Adapter Notes
Vanilla
- Primary binder: bindCombobox.
- Binds explicit DOM nodes and should be cleaned up on unmount.
15. Example Mapping
16. API
Core
-
createComboboxControllervalue -
ComboboxControllertype -
ComboboxControllerOptionstype -
ComboboxKeyboardResulttype -
ComboboxOptionRegistrationtype
Vanilla
-
bindComboboxfunction -
ComboboxBindingOptionsinterface -
ComboboxElementsinterface -
ComboboxPositioningPlacementtype -
ComboboxPositioningReasontype -
ComboboxPositioningRequestinterface