Components
Listbox
Single-selection option list with stable keyboard/a11y semantics.
1. Live Preview
vanilla live preview activates on the client; source HTML keeps a readable preview island and primitive contract.
vanilla listbox 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 { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox(nodes, items, options);
binding.destroy();
4. Examples
Indicator
Show a selected indicator next to options for stronger selection feedback.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "light",
selectionFollowsFocus: false,
onValueChange() {
syncIndicators();
}
});
listboxRoot.setAttribute("aria-label", "Theme listbox");
Disabled
Keep unavailable options visible while preventing interaction and selection.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox(
{ root: listboxRoot, options },
[
{ id: "us", textValue: "United States" },
{ id: "eu", textValue: "Europe", disabled: true },
{ id: "apac", textValue: "APAC" }
],
{ defaultValue: "us", selectionFollowsFocus: false }
);
listboxRoot.setAttribute("aria-label", "Region listbox");
Grouped
Organize options with Group and GroupLabel to improve long-list scanability.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "input",
selectionFollowsFocus: false
});
// options can be grouped with native role=group and separators
listboxRoot.setAttribute("aria-label", "Grouped component listbox");
Rich Content
Compose rich options with OptionMedia, OptionBody, and OptionMeta slots.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "react",
selectionFollowsFocus: false
});
syncIndicators();
listboxRoot.setAttribute("aria-label", "Rich framework listbox");
Multiple
Enable multiple mode to toggle highlighted options with Enter/Space.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const selectedIds = new Set<string>(["read"]);
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "read",
selectionFollowsFocus: false
});
listboxRoot.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
const highlighted = binding.controller.getHighlightedId();
if (highlighted) {
if (selectedIds.has(highlighted)) selectedIds.delete(highlighted);
else selectedIds.add(highlighted);
syncMultipleState();
}
});
selectedLabel.textContent = "Selected:";
Description
Provide list-level and option-level descriptions for clearer decision context.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "static",
selectionFollowsFocus: false
});
listboxRoot.setAttribute("aria-labelledby", "deployment-label");
listboxRoot.setAttribute("aria-describedby", "deployment-description");
listboxRoot.setAttribute("aria-label", "Deployment mode listbox");
Async Loading
Use Loading and Empty slots to represent async data lifecycle clearly.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "alex"
});
const timer = window.setTimeout(() => {
loading.hidden = true;
optionsGroup.hidden = false;
binding.sync();
}, 800);
loading.textContent = "Loading team members...";
Separators
Use Separator for light visual partitions while keeping listbox semantics intact.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "new-file",
selectionFollowsFocus: false
});
// Use separator purely for visual partition
listboxRoot.setAttribute("aria-label", "Quick actions listbox");
Status
Add status markers for quick scanning while preserving textual accessibility.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "todo",
selectionFollowsFocus: false
});
// Option media can include status marker without changing listbox semantics
listboxRoot.setAttribute("aria-label", "Status listbox");
Controlled
Manage selected option with controlled value/onValueChange while keeping selectionFollowsFocus false.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const controllerOptions = {
value: "apple" as string | null,
selectionFollowsFocus: false,
onValueChange(next: string | null) {
controllerOptions.value = next;
updateValueLabel(next);
binding.sync();
}
};
const binding = bindListbox({ root: listboxRoot, options }, registrations, controllerOptions);
valueLabelPrefix.textContent = "Current value:";
Horizontal
Set orientation to horizontal and navigate roving focus with left/right arrows.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "left",
orientation: "horizontal"
});
listboxRoot.setAttribute("aria-label", "Horizontal alignment listbox");
Typeahead
Show listbox typeahead behavior for quickly jumping to matching option text.
import "@nake-ui/theme-vanilla/styles.css";
import { bindListbox } from "@nake-ui/adapter-vanilla";
const binding = bindListbox({ root: listboxRoot, options }, registrations, {
defaultValue: "beijing"
});
// type letters while focus is inside listbox to jump by textValue
listboxRoot.setAttribute("aria-label", "City listbox");
5. RTL
The same structure running inside a `dir="rtl"` container.
vanilla listbox 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
Single-selection option list with stable keyboard/a11y semantics.
- Styling/theme responsibilities
- Virtualization policy in core
8. Anatomy & Slot
rootlabel(optional)description(optional)content(optional)group(optional, repeatable)group-label(optional, insidegroup)option(repeatable)option-indicator(optional, insideoption)option-media(optional, insideoption)option-body(optional, insideoption)option-text(optional, insideoption)option-description(optional, insideoption)option-meta(optional, insideoption)separator(optional, repeatable)empty(optional)loading(optional)
9. DOM Contract
<div data-ui="listbox" data-slot="root" role="listbox" aria-activedescendant="opt-a">
<div id="opt-a" data-slot="option" role="option" aria-selected="true"></div>
</div>
10. State & Events
State Model
- selected option id
- controlled/uncontrolled value
- highlight with optional selection-follows-focus
Event Model
- Arrow/Home/End navigation
- Enter/Space selection commit
11. Keyboard & Focus
- roving focus or active-descendant strategy must stay consistent
12. ARIA
role=listbox/optionaria-selected,aria-activedescendant
13. Validator Rules
-
aria.invalid-activedescendant-target -
state.multiple-active-roving-item
14. Adapter Notes
Vanilla
- Primary binder: bindListbox.
- Binds explicit DOM nodes and should be cleaned up on unmount.
15. Example Mapping
- No direct example mapping yet.
16. API
Core
-
createListboxControllervalue -
ListboxControllertype -
ListboxControllerOptionstype -
ListboxKeyboardResulttype -
ListboxOptionRegistrationtype
Vanilla
-
bindListboxfunction -
ListboxElementsinterface