PixelDropdown

Button-triggered menu of actions with keyboard navigation, typeahead, and a compositional API for advanced layouts.

  • Dual API: declarative items[] sugar and compositional Root/Trigger/Content/Item parts.
  • Item kinds: item, separator, header, checkbox, radio, submenu (chevron affordance).
  • Full keyboard support: arrow navigation, Home/End, Enter/Space activation, printable-key typeahead.
  • Tones + destructive styling, optional shortcut kbd badges, and disabled rows skipped by focus.
  • Pixel and linear surfaces honored across trigger, menu, separators, and shortcut chips.
Status
stable
Category
overlays
Since
v1.0.0

API

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

PixelDropdown

PropTypeDefaultDescription
labelstring—Trigger button label (legacy items[] API).
itemsDropdownOption[]—Items to render (legacy items[] API).
onSelect(value: string) => void—Called with the selected item's value (legacy items[] API).
tone'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink' | 'neutral''neutral'Trigger tone.
iconReact.ReactNode—Optional icon at the right side of the trigger button. Defaults to a chevron.
disabledbooleanfalseDisables the trigger button.
surface'pixel' | 'linear'—Visual surface override. Falls back to nearest <PxlKitProvider> surface.
ariaLabelstring—ARIA label override when the trigger label is purely decorative.
childrenReact.ReactNode—When using compositional API, children replace items[] rendering.

ref points to <div>.

PixelDropdown.Root

PropTypeDefaultDescription
openboolean—Whether the menu is open; leave unset for an uncontrolled menu.
defaultOpenbooleanfalseInitial open state while uncontrolled.
onOpenChange(open: boolean) => void—Called with every open state the menu asks for (the trigger and its keys, Escape, a press outside, an item).
surface'pixel' | 'linear'—Surface override for the trigger and the menu; defaults to the nearest provider.
children (required)React.ReactNode—The trigger and the menu (PixelDropdown.Trigger, PixelDropdown.Content).

PixelDropdown.Trigger

PropTypeDefaultDescription
childrenReact.ReactNode—Button label.
tone'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink' | 'neutral''neutral'Button tone.
iconReact.ReactNode—Icon at the end of the button, in place of the chevron.
disabledbooleanfalseDisables the button.
ariaLabelstring—Accessible label, for a label that is only decorative.
idstring—Id of the button, which names the menu; generated when left out.

ref points to <button>.

PixelDropdown.Content

PropTypeDefaultDescription
childrenReact.ReactNode—The items.
classNamestring—Extra classes on the menu.

ref points to <div>.

PixelDropdown.Item

PropTypeDefaultDescription
valuestring—Item identity for highlight/typeahead registration (NOT the HTML form value).
children (required)React.ReactNode—Label.
onSelect() => void—Selection callback (NOT the DOM select event).
disabledbooleanfalseSkipped by the keyboard and ignores the pointer.
destructivebooleanfalseVisually marks the row red; equivalent to tone="red".
tone'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink' | 'neutral'—Text tone.
iconReact.ReactNode—Icon before the label (plain items).
shortcutstring—Keyboard hint shown at the end of the row (display only).

Also takes the native attributes and event handlers of <button> (ButtonHTMLAttributes<HTMLButtonElement>).

ref points to <button>.

PixelDropdown.Separator

No props of its own.

PixelDropdown.Header

PropTypeDefaultDescription
children (required)React.ReactNode—Text of the header row.

PixelDropdown.CheckboxItem

PropTypeDefaultDescription
checkedboolean—Shows the check mark, and sets aria-checked.
children (required)React.ReactNode—Label.
onSelect() => void—Selection callback (NOT the DOM select event).
valuestring—Item identity for highlight/typeahead registration (NOT the HTML form value).
disabledboolean—Skipped by the keyboard and ignores the pointer.
tone'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink' | 'neutral'—Text tone.
destructiveboolean—Visually marks the row red; equivalent to tone="red".
shortcutstring—Keyboard hint shown at the end of the row (display only).

Also takes the native attributes and event handlers of <button> (ButtonHTMLAttributes<HTMLButtonElement>), except icon.

ref points to <button>.

PixelDropdown.RadioItem

PropTypeDefaultDescription
checkedboolean—Shows the dot, and sets aria-checked.
children (required)React.ReactNode—Label.
onSelect() => void—Selection callback (NOT the DOM select event).
valuestring—Item identity for highlight/typeahead registration (NOT the HTML form value).
disabledboolean—Skipped by the keyboard and ignores the pointer.
tone'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink' | 'neutral'—Text tone.
destructiveboolean—Visually marks the row red; equivalent to tone="red".
shortcutstring—Keyboard hint shown at the end of the row (display only).

Also takes the native attributes and event handlers of <button> (ButtonHTMLAttributes<HTMLButtonElement>), except icon.

ref points to <button>.

Accessibility

WCAG target: 2.1 AA

  • menu
  • button

Trigger exposes aria-haspopup="menu", aria-expanded, and aria-controls wired to the menu id while the menu is open, and names the menu through aria-labelledby (its own id, or a generated one). The open menu takes focus (tabindex="-1") and points aria-activedescendant at the highlighted item, so assistive technology follows the arrows, Home/End and typeahead. Items use role="menuitem" — menuitemcheckbox and menuitemradio with aria-checked for checkbox and radio rows — with aria-disabled for skipped rows. Separators use role="separator"; headers are role="presentation". Escape, choosing an item and Tab return focus to the trigger; a press outside closes the menu and leaves focus where the pointer put it.

Keyboard

KeyActionWhen
Enter / SpaceOpen the menu and move focus into it.trigger focused
ArrowDownOpen the menu with focus in it, highlighting the first enabled item.trigger focused
ArrowUpOpen the menu with focus in it, highlighting the last enabled item.trigger focused
ArrowDownHighlight the next enabled item; stops at the last.menu open
ArrowUpHighlight the previous enabled item; stops at the first.menu open
HomeHighlight the first enabled item.menu open
EndHighlight the last enabled item.menu open
EnterActivate the highlighted item, close the menu and return focus to the trigger.menu open
SpaceActivate the highlighted item, close the menu and return focus to the trigger.menu open
EscapeClose the menu, clear the highlight and return focus to the trigger.menu open
TabClose the menu and move focus on from the trigger to the next focusable element.menu open
Shift+TabClose the menu and move focus back from the trigger to the previous focusable element.menu open
a-z / 0-9Typeahead — jump to the first item whose label starts with the typed prefix.menu open

Usage

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

export function Default() {
  return (
    <PixelDropdown
      label="Actions"
      items={[
        { value: 'edit', label: 'Edit' },
        { value: 'duplicate', label: 'Duplicate' },
        { value: 'archive', label: 'Archive' },
      ]}
      onSelect={() => {}}
    />
  );
}

Examples

Default

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

export function Default() {
  return (
    <PixelDropdown
      label="Actions"
      items={[
        { value: 'edit', label: 'Edit' },
        { value: 'duplicate', label: 'Duplicate' },
        { value: 'archive', label: 'Archive' },
      ]}
      onSelect={() => {}}
    />
  );
}

Tones

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

export function Tones() {
  return (
    <div className="flex flex-wrap items-start gap-4">
      <PixelDropdown
        label="Neutral"
        tone="neutral"
        items={[{ value: 'a', label: 'Option A' }]}
      />
      <PixelDropdown
        label="Cyan"
        tone="cyan"
        items={[{ value: 'a', label: 'Option A' }]}
      />
      <PixelDropdown
        label="Green"
        tone="green"
        items={[{ value: 'a', label: 'Option A' }]}
      />
      <PixelDropdown
        label="Red"
        tone="red"
        items={[{ value: 'a', label: 'Option A' }]}
      />
    </div>
  );
}

Surfaces

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

export function Surfaces() {
  return (
    <div className="flex flex-wrap items-start gap-4">
      <PixelDropdown
        label="Pixel"
        surface="pixel"
        items={[
          { value: 'one', label: 'First' },
          { value: 'two', label: 'Second' },
        ]}
      />
      <PixelDropdown
        label="Linear"
        surface="linear"
        items={[
          { value: 'one', label: 'First' },
          { value: 'two', label: 'Second' },
        ]}
      />
    </div>
  );
}

Disabled trigger

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

export function Disabled() {
  return (
    <PixelDropdown
      label="Unavailable"
      disabled
      items={[{ value: 'a', label: 'Option A' }]}
    />
  );
}

Shortcuts

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

export function WithIconsAndShortcuts() {
  return (
    <PixelDropdown
      label="File"
      items={[
        { value: 'new', label: 'New', shortcut: 'Ctrl+N' },
        { value: 'open', label: 'Open…', shortcut: 'Ctrl+O' },
        { value: 'save', label: 'Save', shortcut: 'Ctrl+S' },
      ]}
      onSelect={() => {}}
    />
  );
}

Headers + separators

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

export function HeadersAndSeparators() {
  return (
    <PixelDropdown
      label="Account"
      items={[
        { value: 'h1', label: 'Profile', kind: 'header' },
        { value: 'view', label: 'View profile' },
        { value: 'edit', label: 'Edit profile' },
        { value: 'sep1', label: '', kind: 'separator' },
        { value: 'h2', label: 'Danger Zone', kind: 'header' },
        { value: 'delete', label: 'Delete account', tone: 'red' },
      ]}
      onSelect={() => {}}
    />
  );
}

Checkbox + radio items

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

export function CheckboxAndRadio() {
  return (
    <PixelDropdown
      label="View"
      items={[
        { value: 'g1', label: 'Show', kind: 'header' },
        { value: 'grid', label: 'Grid lines', kind: 'checkbox', checked: true },
        { value: 'ruler', label: 'Ruler', kind: 'checkbox', checked: false },
        { value: 'sep', label: '', kind: 'separator' },
        { value: 'g2', label: 'Density', kind: 'header' },
        { value: 'compact', label: 'Compact', kind: 'radio', checked: false },
        { value: 'cozy', label: 'Cozy', kind: 'radio', checked: true },
        { value: 'spacious', label: 'Spacious', kind: 'radio', checked: false },
      ]}
      onSelect={() => {}}
    />
  );
}

Disabled items

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

export function DisabledItems() {
  return (
    <PixelDropdown
      label="Edit"
      items={[
        { value: 'undo', label: 'Undo', shortcut: 'Ctrl+Z' },
        { value: 'redo', label: 'Redo', shortcut: 'Ctrl+Y', disabled: true },
        { value: 'sep', label: '', kind: 'separator' },
        { value: 'cut', label: 'Cut' },
        { value: 'copy', label: 'Copy' },
        { value: 'paste', label: 'Paste', disabled: true },
      ]}
      onSelect={() => {}}
    />
  );
}

Compositional API

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

export function Composition() {
  return (
    <PixelDropdown.Root>
      <PixelDropdown.Trigger>Menu</PixelDropdown.Trigger>
      <PixelDropdown.Content>
        <PixelDropdown.Header>Project</PixelDropdown.Header>
        <PixelDropdown.Item value="rename" onSelect={() => {}}>
          Rename
        </PixelDropdown.Item>
        <PixelDropdown.Item value="share" onSelect={() => {}} shortcut="Ctrl+E">
          Share
        </PixelDropdown.Item>
        <PixelDropdown.Separator />
        <PixelDropdown.Item value="delete" onSelect={() => {}} destructive>
          Delete
        </PixelDropdown.Item>
      </PixelDropdown.Content>
    </PixelDropdown.Root>
  );
}

Controlled open

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

export function ControlledOpen() {
  const [open, setOpen] = useState(false);
  return (
    <PixelDropdown.Root open={open} onOpenChange={setOpen}>
      <PixelDropdown.Trigger>{open ? 'Close' : 'Open'} menu</PixelDropdown.Trigger>
      <PixelDropdown.Content>
        <PixelDropdown.Item value="one" onSelect={() => {}}>
          One
        </PixelDropdown.Item>
        <PixelDropdown.Item value="two" onSelect={() => {}}>
          Two
        </PixelDropdown.Item>
      </PixelDropdown.Content>
    </PixelDropdown.Root>
  );
}

Related