PixelSplitButton

Composite button pairing a primary action with a chevron-triggered dropdown menu for related secondary actions.

  • Primary click handler plus a menu of alternate actions in a single control
  • Inherits tone + surface theming from the design system
  • Its menu closes on an outside press, Escape or Tab
  • aria-haspopup="menu" + aria-expanded on the chevron trigger
Status
stable
Category
actions
Since
v1.0.0

API

import { PixelSplitButton } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
label (required)string—Text shown on the primary (left) button.
options (required)Option[]—Options shown in the dropdown menu.
tone'green' | 'cyan' | 'gold' | 'red' | 'purple' | 'pink' | 'neutral''purple'Color tone (maps to toneMap).
surface'pixel' | 'linear'—Surface aesthetic override; defaults to nearest provider.
disabledbooleanfalseWhen true, both primary button and chevron trigger are disabled.
onPrimary() => void—Fires when the primary (label) button is clicked.
onSelect(value: string) => void—Fires with the selected option's value when a menu item is chosen.

ref points to <div>.

Accessibility

WCAG target: 2.1 AA

  • menu
  • button

Follows the WAI-ARIA menu button pattern, as PixelDropdown does. The chevron is a button of its own, named "More options", with aria-haspopup="menu", aria-expanded, and aria-controls wired to the menu while it is open. The menu (role="menu", named by the chevron through aria-labelledby) takes focus as it opens (tabindex="-1") and points aria-activedescendant at the highlighted option, so assistive technology follows the arrows, Home/End and typeahead; its role="menuitem" options are not tab stops. Escape, Tab and choosing an option return focus to the chevron; a press outside closes the menu and leaves focus where the pointer put it. The frame clips both halves, so each shows keyboard focus inside its own edge.

Keyboard

KeyActionWhen
Enter / SpaceRun the primary action (onPrimary).primary button focused
Enter / SpaceOpen the menu and move focus into it; close it when it is open.chevron focused
ArrowDownOpen the menu with focus in it, highlighting the first option.chevron focused
ArrowUpOpen the menu with focus in it, highlighting the last option.chevron focused
ArrowDownHighlight the next option; stops at the last.menu open
ArrowUpHighlight the previous option; stops at the first.menu open
HomeHighlight the first option.menu open
EndHighlight the last option.menu open
Enter / SpaceChoose the highlighted option (onSelect), close the menu and return focus to the chevron.menu open
EscapeClose the menu and return focus to the chevron.menu open
TabClose the menu and move focus on from the chevron.menu open
Shift+TabClose the menu and move focus back from the chevron to the primary button.menu open
a-z / 0-9Typeahead — highlight the first option whose label starts with the typed prefix.menu open

Usage

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

const exportOptions = [
  { value: 'png', label: 'Export as PNG' },
  { value: 'svg', label: 'Export as SVG' },
  { value: 'json', label: 'Export icon code' },
];

export function Default() {
  return (
    <PixelSplitButton
      label="Export"
      options={exportOptions}
      onPrimary={() => {}}
      onSelect={() => {}}
    />
  );
}

Examples

Default

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

const exportOptions = [
  { value: 'png', label: 'Export as PNG' },
  { value: 'svg', label: 'Export as SVG' },
  { value: 'json', label: 'Export icon code' },
];

export function Default() {
  return (
    <PixelSplitButton
      label="Export"
      options={exportOptions}
      onPrimary={() => {}}
      onSelect={() => {}}
    />
  );
}

Tones

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

const exportOptions = [
  { value: 'png', label: 'Export as PNG' },
  { value: 'svg', label: 'Export as SVG' },
  { value: 'json', label: 'Export icon code' },
];

export function Tones() {
  return (
    <div className="flex flex-wrap gap-3">
      <PixelSplitButton label="Green" tone="green" options={exportOptions} />
      <PixelSplitButton label="Cyan" tone="cyan" options={exportOptions} />
      <PixelSplitButton label="Gold" tone="gold" options={exportOptions} />
      <PixelSplitButton label="Red" tone="red" options={exportOptions} />
      <PixelSplitButton label="Purple" tone="purple" options={exportOptions} />
      <PixelSplitButton label="Pink" tone="pink" options={exportOptions} />
      <PixelSplitButton label="Neutral" tone="neutral" options={exportOptions} />
    </div>
  );
}

Surfaces

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

const exportOptions = [
  { value: 'png', label: 'Export as PNG' },
  { value: 'svg', label: 'Export as SVG' },
  { value: 'json', label: 'Export icon code' },
];

export function Surfaces() {
  return (
    <div className="flex flex-wrap gap-3">
      <PixelSplitButton label="Pixel" surface="pixel" options={exportOptions} />
      <PixelSplitButton label="Linear" surface="linear" options={exportOptions} />
    </div>
  );
}

Disabled

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

const exportOptions = [
  { value: 'png', label: 'Export as PNG' },
  { value: 'svg', label: 'Export as SVG' },
  { value: 'json', label: 'Export icon code' },
];

export function Disabled() {
  return (
    <PixelSplitButton
      label="Export"
      options={exportOptions}
      disabled
    />
  );
}

With Callbacks

import React from 'react';
import { PixelSplitButton } from '@pxlkit/ui-kit';

export function WithCallbacks() {
  const [last, setLast] = React.useState<string>('—');
  return (
    <div className="flex flex-col items-start gap-2">
      <PixelSplitButton
        label="Save"
        tone="cyan"
        options={[
          { value: 'draft', label: 'Save as draft' },
          { value: 'template', label: 'Save as template' },
          { value: 'copy', label: 'Save a copy' },
        ]}
        onPrimary={() => setLast('primary')}
        onSelect={(v) => setLast(v)}
      />
      <span className="text-xs text-retro-muted">last action: {last}</span>
    </div>
  );
}

Related