PixelSelect

Custom single-value dropdown built on a button + listbox (no native <select>) with full keyboard navigation and tone/size/surface theming.

  • WAI-ARIA combobox + listbox semantics — aria-expanded, aria-haspopup, aria-selected wired to the trigger and options.
  • Full keyboard support: ArrowUp/Down, Home/End, Enter/Space to select, Escape to close, Tab to dismiss.
  • Controlled — value + onChange (React), v-model (Vue), [(value)] or forms (Angular) — or uncontrolled with defaultValue; the value is the selected option value, as a string.
  • Form-friendly — hidden mirror input lets the value participate in native <form> submissions via name.
  • Tone, size, and surface (pixel/linear) variants share the same primitives as the rest of the input family.
Status
stable
Category
forms
Since
v1.0.0

API

import { PixelSelect } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
labelstring—Floating label rendered above the trigger.
options (required)Option[]—Items rendered in the listbox.
valuestring—Controlled value. Make sure to update via onChange.
defaultValuestring—Uncontrolled initial value.
onChange(value: string) => void—Fires when the user picks an option.
placeholderstring'Select...'Placeholder shown when no value is selected.
hintstring—Helper text shown below the field. Hidden when error is set.
errorstring—Error message shown below the field; flips visual state to invalid.
disabledbooleanfalseDisables interaction + grays out the trigger.
tone'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink' | 'neutral''neutral'Visual tone for focus ring + selected option. Default: 'neutral'.
size'sm' | 'md' | 'lg''md'Trigger height token. Default: 'md'.
surface'pixel' | 'linear'—Surface variant. Inherits from PxlKitSurfaceProvider when omitted.
namestring—Sets name on the hidden serialization input so the value participates in native <form> submissions.
requiredboolean—Marks the field as required for native form validation.
idstring—DOM id forwarded to the trigger.
aria-describedbystring—aria-describedby of the trigger; the hint / error is added while it shows.

ref points to <button>.

Accessibility

WCAG target: 2.1 AA

  • combobox
  • listbox

Trigger is a <button role="combobox"> paired with a role="listbox" popup containing role="option" children. Provide an accessible name via label; the hint/error it shows is linked through aria-describedby, after any ids you pass, and error automatically sets aria-invalid.

Keyboard

KeyActionWhen
EnterOpens the listbox, or selects the highlighted option when open.—
SpaceOpens the listbox, or selects the highlighted option when open.—
ArrowDownOpens the listbox or moves highlight to the next option.—
ArrowUpMoves highlight to the previous option.—
HomeHighlights the first option (opening the listbox if needed).—
EndHighlights the last option (opening the listbox if needed).—
EscapeCloses the listbox without changing the selection.—
TabCloses the listbox and moves focus to the next focusable element.—

Usage

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

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
];

export function Default() {
  return (
    <PixelSelect
      label="Fruit"
      options={FRUITS}
      placeholder="Pick a fruit"
      hint="Choose your favorite"
    />
  );
}

Examples

Default

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

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
];

export function Default() {
  return (
    <PixelSelect
      label="Fruit"
      options={FRUITS}
      placeholder="Pick a fruit"
      hint="Choose your favorite"
    />
  );
}

Uncontrolled

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

const REGIONS = [
  { value: 'us', label: 'United States' },
  { value: 've', label: 'Venezuela' },
  { value: 'es', label: 'Spain' },
  { value: 'mx', label: 'Mexico' },
];

export function Uncontrolled() {
  return (
    <PixelSelect
      label="Region"
      options={REGIONS}
      defaultValue="ve"
    />
  );
}

Controlled

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

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
];

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

Tones

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

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
];

export function Tones() {
  return (
    <div className="flex flex-col gap-3">
      <PixelSelect label="Neutral" options={FRUITS} tone="neutral" defaultValue="apple" />
      <PixelSelect label="Cyan" options={FRUITS} tone="cyan" defaultValue="apple" />
      <PixelSelect label="Green" options={FRUITS} tone="green" defaultValue="apple" />
      <PixelSelect label="Gold" options={FRUITS} tone="gold" defaultValue="apple" />
      <PixelSelect label="Purple" options={FRUITS} tone="purple" defaultValue="apple" />
      <PixelSelect label="Pink" options={FRUITS} tone="pink" defaultValue="apple" />
    </div>
  );
}

Sizes

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

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
];

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

Surfaces

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

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
];

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

Disabled

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

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
];

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

With error

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

const REGIONS = [
  { value: 'us', label: 'United States' },
  { value: 've', label: 'Venezuela' },
  { value: 'es', label: 'Spain' },
  { value: 'mx', label: 'Mexico' },
];

export function WithError() {
  return (
    <PixelSelect
      label="Region"
      options={REGIONS}
      placeholder="Pick one"
      error="Please choose a region"
      tone="red"
    />
  );
}

Required

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

const REGIONS = [
  { value: 'us', label: 'United States' },
  { value: 've', label: 'Venezuela' },
  { value: 'es', label: 'Spain' },
  { value: 'mx', label: 'Mexico' },
];

export function Required() {
  return (
    <PixelSelect
      label="Region"
      options={REGIONS}
      name="region"
      required
      placeholder="Required field"
    />
  );
}

With form name

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

const FRUITS = [
  { value: 'apple', label: 'Apple' },
  { value: 'banana', label: 'Banana' },
  { value: 'cherry', label: 'Cherry' },
  { value: 'date', label: 'Date' },
];

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

Related