Components

Listbox

Single-selection option list with stable keyboard/a11y semantics.

Release Scope: Canonical Implementation: Core Site Visibility: Listed Adapters: React, Vanilla, Solid, Vue, Svelte Category: selection

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.

Theme

Selected indicator gives explicit visual feedback beside each option.

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.

Region

Disabled options remain visible and are skipped during keyboard navigation.

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.

Component
Inputs
Overlays

Group options to reduce scanning cost in long lists.

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.

Framework

Each option can include media, main text, description, and meta badges.

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.

Permissions

Press Enter or Space to toggle selection on highlighted options.

Selected:

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.

Deployment Mode

Choose how this app should be deployed.

Provide guidance on both component level and option level.

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.

Assignee
Loading team members...

Loading and empty states make async listbox behavior explicit.

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.

Quick Actions

Separators create visual partitions without introducing grouped semantics.

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.

Status

Color marks improve quick scanning while text keeps exact semantics.

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.

Fruit

Controlled value stays synchronized with external updates.

Current value:

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.

Alignment

Use left and right arrows to move between horizontal options.

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.

With focus on the list, type letters to jump by typeahead.

City
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

  • root
  • label (optional)
  • description (optional)
  • content (optional)
  • group (optional, repeatable)
  • group-label (optional, inside group)
  • option (repeatable)
  • option-indicator (optional, inside option)
  • option-media (optional, inside option)
  • option-body (optional, inside option)
  • option-text (optional, inside option)
  • option-description (optional, inside option)
  • option-meta (optional, inside option)
  • 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/option
  • aria-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

  • createListboxController value
  • ListboxController type
  • ListboxControllerOptions type
  • ListboxKeyboardResult type
  • ListboxOptionRegistration type

Vanilla

  • bindListbox function
  • ListboxElements interface