@basenative/combobox

中文文档
3.0.0发布于 2026/10/31Apache-2.0TypeScriptES Module零依赖构建来源已验证

Accessible combobox primitive — typeahead input that filters an existing list and lets the user create a new entry. WAI-ARIA combobox listbox pattern.

原始使用文档
文档目录13 节
  1. Why
  2. Install
  3. Quick start — pick from existing options
  4. Allow creating new entries
  5. With option hints
  6. Controlled with a signal
  7. ARIA pattern
  8. Keyboard
  9. Theming
  10. Filter strategies
  11. Mobile testing notes
  12. API
  13. License

@basenative/combobox

Accessible typeahead combobox primitive — pick from an existing list, or create a new entry. WAI-ARIA APG combobox+listbox pattern, SSR-first, signal-driven, themable.

Part of the BaseNative ecosystem.

Why

Most combobox libraries are huge (40+ KB), framework-specific, and ship with opinionated styles you have to fight. This one is the opposite:

  • Renders semantic HTML server-side. Without JS, you get a plain <input> + <datalist> typeahead (good enough for forms).
  • With JS, hydrates into a full APG-pattern combobox with virtual focus, keyboard nav, and a built-in "+ Create" affordance.
  • Themes entirely through --cb-* CSS custom properties. Zero bundled palette assumptions.
  • Layout-shift-free: opening the listbox never reflows the page.
  • Signal interop via runtime.effect — pass a @basenative/runtime signal and the input mirrors it.

Install

pnpm add @basenative/combobox @basenative/runtime

Quick start — pick from existing options

import { Combobox } from '@basenative/combobox';
import '@basenative/combobox/css';

const cb = Combobox({
  id: 'fruit',
  name: 'fruit',
  label: 'Pick a fruit',
  options: ['Apple', 'Banana', 'Cherry'],
  onChange: (value) => console.log('selected', value),
});

document.querySelector('#mount').innerHTML = cb.html;
const handle = cb.hydrate(document.querySelector('[data-bn="combobox"]'));

Allow creating new entries

const cb = Combobox({
  id: 'tag',
  label: 'Tag',
  options: existingTags,
  allowCreate: true,
  createLabel: (input) => `+ New tag "${input}"`,
  onChange: (value) => setTag(value),
  onCreate: (label) => {
    addTag(label);   // persist somewhere
    setTag(label);
  },
});

If you don't pass onCreate, BaseNative will treat the user's typed text as the value and call onChange(typed).

With option hints

Useful for showing keyboard shortcuts, sort metadata, or contextual info inline:

const cb = Combobox({
  options: [
    { value: 'today',     label: 'Today',          hint: 'T' },
    { value: 'this-week', label: 'This week',      hint: 'W' },
    { value: 'all',       label: 'All time',       hint: 'A' },
  ],
  onChange: (v) => setRange(v),
});

Controlled with a signal

import { signal, effect } from '@basenative/runtime';
import { Combobox } from '@basenative/combobox';

const category = signal('books');

const cb = Combobox({
  options: ['books', 'movies', 'music'],
  value: category,            // function-shape signal accessor
  onChange: (v) => category.set(v),
  runtime: { effect },        // mirrors signal -> input on every change
});

ARIA pattern

This package implements the W3C APG combobox/listbox pattern precisely:

  • The input has role="combobox", aria-autocomplete="list", aria-expanded, aria-controls=<listbox id>.
  • The popup has role="listbox". Each row has role="option" and aria-selected.
  • Focus stays in the input. Highlighted row is communicated via aria-activedescendant (virtual focus).
    • This is essential on mobile: moving real focus into the popup would close the soft keyboard.
  • A polite live region (aria-live="polite") announces option count after each filter change, and announces the create-option hint separately so screen-reader users know it's a different action.
  • The create-option uses a distinct color token (--cb-create-fg) and data-bn-cb-create="true" for SR / styling.

Keyboard

KeyBehavior
ArrowDownOpen popup (if closed) and move active option down
ArrowUpOpen popup (if closed) and move active option up
HomeMove active to first option
EndMove active to last option
EnterCommit the active option (or, with no active row, the typed query)
EscapeClose popup, leave input value intact
TabCommit active option (if any), then move focus per natural tab order

Selecting "+ Create" never wipes the user's typed input — the typed text becomes the new value.

Theming

Override at any ancestor:

.dark [data-bn="combobox"] {
  --cb-bg: #18181b;
  --cb-fg: #f4f4f5;
  --cb-border: #3f3f46;
  --cb-border-focus: #f59e0b;
  --cb-listbox-bg: #18181b;
  --cb-listbox-border: #3f3f46;
  --cb-option-bg-hover: #27272a;
  --cb-option-bg-active: #3f3f46;
  --cb-create-fg: #fbbf24;
  --cb-create-bg: #422006;
}
TokenPurposeDefault
--cb-bgInput field background#ffffff
--cb-fgDefault text#111111
--cb-borderField border#c8c6bf
--cb-border-focusField border on focus#2E5BFF
--cb-radiusField corner radius6px
--cb-input-hField height44px
--cb-listbox-bgPopup background#ffffff
--cb-listbox-borderPopup border#c8c6bf
--cb-option-bg-hoverOption hover background#f1efe9
--cb-option-bg-activeOption active (keyboard) background#e6e4dc
--cb-option-fgOption text#111111
--cb-create-fgCreate-option text (distinct on purpose)#0F5132
--cb-create-bgCreate-option background#ECFDF3
--cb-min-tapMinimum hit target (WCAG 2.5.5 AA)44px

Filter strategies

import { Combobox, defaultFilter, prefixFilter, fuzzyFilter } from '@basenative/combobox';

Combobox({ options, filter: prefixFilter });    // type "ap" → "Apple"
Combobox({ options, filter: fuzzyFilter });     // type "apl" → "Apple"
Combobox({ options, filter: (o, q) => o.value.startsWith(q.toUpperCase()) });

Mobile testing notes

  • iOS 16/17 Safari: tested on iPhone 12 / 14 Pro. Tapping options doesn't dismiss the soft keyboard (we cancel mousedown on the listbox).
  • inputmode="text" keeps the system keyboard layout text-shaped (not a numpad).
  • font-size: 16px minimum prevents iOS zoom-on-focus.
  • 44pt min hit target on every option.

API

import {
  Combobox,            // factory: { html, hydrate(rootEl) }
  renderCombobox,      // SSR: returns HTML string
  hydrateCombobox,     // wire reactivity to an existing root
  normalizeOption,     // (raw) => { value, label, hint? }
  defaultFilter,
  prefixFilter,
  fuzzyFilter,
  applyAriaAttributes,
  escapeOnKeydown,
  announce,
} from '@basenative/combobox';

See types/index.d.ts for full signatures.

License

Apache-2.0

版本与发布记录

@basenative/combobox 共发布 11 个版本,首次发布于 2026/10/18;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
3.0.0latest2026/10/312 天前
2.4.02026/10/268 天前
2.3.22026/10/2212 天前
2.3.12026/10/2212 天前
2.3.02026/10/2212 天前
2.2.02026/10/2113 天前

关于 @basenative/combobox 的常见问题

@basenative/combobox 是什么?

可访问的组合框原语——预先输入过滤现有列表并允许用户创建新条目的输入。WAI-ARIA组合框列表框模式。

如何安装 @basenative/combobox?

在项目目录运行 npm install @basenative/combobox,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@basenative/runtime(3.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@basenative/combobox 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@basenative/combobox 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@basenative/combobox 可以商用吗?

@basenative/combobox 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@basenative/combobox 还在维护吗?

最新版本 3.0.0 发布于 2026/10/31(2 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/10/18。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。