PixelCombobox

Searchable single-value combobox built on a button trigger + listbox popover with type-to-filter, optional grouping, and full keyboard navigation.

  • WAI-ARIA combobox pattern — role="combobox" trigger paired with a role="listbox" popup and aria-activedescendant for highlight tracking.
  • Type-to-filter search input is opt-out (searchable set to false) for short lists where filtering adds friction.
  • Optional group field on options renders sticky group headings in the listbox without breaking keyboard navigation.
  • Controlled — value + onChange (React), v-model (Vue), [(value)] or forms (Angular) — or uncontrolled with defaultValue; a hidden name input joins native forms.
  • Tone-free surface theming (pixel/linear) and shared size scale (sm/md/lg) match the rest of the input family.
Status
stable
Category
forms
Since
v1.8.0

API

import { PixelCombobox } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
valuestring—Selected value; leave unset for an uncontrolled combobox.
defaultValuestring—Initial value while uncontrolled.
onChange(next: string) => void—Called with the value of the option the user selected.
options (required)PixelComboboxOption[]—The options of the listbox.
searchablebooleantrueShows the search field that filters the options.
placeholderstring'Select…'Text shown while nothing is selected.
emptyMessagestring'No results.'Shown in place of the listbox when nothing matches the search.
disabledbooleanfalseDisables the combobox and greys out the trigger.
size'sm' | 'md' | 'lg''md'Trigger height.
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.
labelstring—Label rendered above the trigger.
hintstring—Helper text below the field; hidden while error is set.
errorstring—Error message below the field; marks the trigger invalid.
namestring—Form field name — a hidden input submits the value.
idstring—id of the trigger; generated when left out.

ref points to <button>.

Accessibility

WCAG target: 2.1 AA

  • combobox
  • listbox

Trigger is a <button role="combobox"> with aria-haspopup="listbox", aria-expanded, and aria-activedescendant wired to the highlighted option id. When searchable, an inner role="searchbox" input proxies keyboard navigation to the listbox. Provide an accessible name via label; error automatically sets aria-invalid.

Keyboard

KeyActionWhen
ArrowDownOpens the listbox when closed; moves highlight to the next option when open.—
ArrowUpOpens the listbox when closed; moves highlight to the previous option when open.—
HomeHighlights the first option in the filtered list.—
EndHighlights the last option in the filtered list.—
EnterOpens the listbox when closed; commits the highlighted option and closes the listbox when open.—
EscapeCloses the listbox without changing the selection.—

Usage

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function Default() {
  return (
    <PixelCombobox
      label="Fruit"
      options={FRUITS}
      placeholder="Pick a fruit"
      hint="Type to filter"
    />
  );
}

Examples

Default

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function Default() {
  return (
    <PixelCombobox
      label="Fruit"
      options={FRUITS}
      placeholder="Pick a fruit"
      hint="Type to filter"
    />
  );
}

Uncontrolled

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function Uncontrolled() {
  return (
    <PixelCombobox
      label="Fruit"
      options={FRUITS}
      defaultValue="banana"
    />
  );
}

Controlled

import { useState } from 'react';
import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function Controlled() {
  const [value, setValue] = useState('cherry');
  return (
    <PixelCombobox
      label="Controlled fruit"
      options={FRUITS}
      value={value}
      onChange={setValue}
    />
  );
}

Grouped options

import { PixelCombobox } from '@pxlkit/ui-kit';

const GROUPED = [
  { value: 'us', label: 'United States', group: 'Americas' },
  { value: 've', label: 'Venezuela', group: 'Americas' },
  { value: 'mx', label: 'Mexico', group: 'Americas' },
  { value: 'es', label: 'Spain', group: 'Europe' },
  { value: 'fr', label: 'France', group: 'Europe' },
  { value: 'de', label: 'Germany', group: 'Europe' },
  { value: 'jp', label: 'Japan', group: 'Asia' },
  { value: 'kr', label: 'South Korea', group: 'Asia' },
];

export function Grouped() {
  return (
    <PixelCombobox
      label="Country"
      options={GROUPED}
      placeholder="Pick a country"
    />
  );
}

Without search

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function NotSearchable() {
  return (
    <PixelCombobox
      label="Fruit"
      options={FRUITS}
      searchable={false}
      placeholder="No filter"
    />
  );
}

Sizes

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function Sizes() {
  return (
    <div className="flex flex-col gap-3">
      <PixelCombobox label="Small" options={FRUITS} size="sm" placeholder="sm" />
      <PixelCombobox label="Medium" options={FRUITS} size="md" placeholder="md" />
      <PixelCombobox label="Large" options={FRUITS} size="lg" placeholder="lg" />
    </div>
  );
}

Surfaces

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function Surfaces() {
  return (
    <div className="flex flex-col gap-3">
      <PixelCombobox label="Pixel" options={FRUITS} surface="pixel" placeholder="pixel surface" />
      <PixelCombobox label="Linear" options={FRUITS} surface="linear" placeholder="linear surface" />
    </div>
  );
}

Disabled

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function Disabled() {
  return (
    <PixelCombobox
      label="Disabled"
      options={FRUITS}
      defaultValue="apple"
      disabled
    />
  );
}

With error

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function WithError() {
  return (
    <PixelCombobox
      label="Fruit"
      options={FRUITS}
      placeholder="Pick one"
      error="Please choose a fruit"
    />
  );
}

With form name

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function WithFormName() {
  return (
    <form>
      <PixelCombobox
        label="Fruit"
        options={FRUITS}
        name="fruit"
        defaultValue="cherry"
        hint="Value participates in native form submission"
      />
    </form>
  );
}

Custom empty message

import { PixelCombobox } from '@pxlkit/ui-kit';

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
  { value: 'elderberry', label: 'Elderberry' },
  { value: 'fig', label: 'Fig' },
  { value: 'grape', label: 'Grape' },
];

export function CustomEmptyMessage() {
  return (
    <PixelCombobox
      label="Fruit"
      options={FRUITS}
      placeholder="Type 'xyz'"
      emptyMessage="No fruits match your filter"
    />
  );
}

Related