Skip to main content

Button

Button triggers an action when clicked. It wraps the Spar headless button primitive and adds Takeoff visual vocabulary — variant, size, loading state, and icon slots.

Usage​

import { Button } from '@takeoff-ui/react-spar';
<Button variant="primary" size="base">
Click me
</Button>

Playground​


function PlaygroundDemo() {
  return (
    <Button>Primary</Button>
  );
}

render(<PlaygroundDemo />);

Variants​


function VariantsDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="neutral">Neutral</Button>
      <Button variant="info">Info</Button>
      <Button variant="success">Success</Button>
      <Button variant="danger">Danger</Button>
      <Button variant="warning">Warning</Button>
      <Button variant="white">White</Button>
      <Button variant="black">Black</Button>
    </div>
  );
}

render(<VariantsDemo />);

Appearance​


function AppearanceDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3 items-center">
      <Button appearance="filled">Filled</Button>
      <Button appearance="filledLight">FilledLight</Button>
      <Button appearance="outlined">Outlined</Button>
      <Button appearance="text">Text</Button>
    </div>
  );
}

render(<AppearanceDemo />);

Sizes​


function SizesDemo() {
  return (
    <div className="flex flex-wrap justify-center items-center gap-3">
      <Button size="small">Small</Button>
      <Button size="base">Base</Button>
      <Button size="large">Large</Button>
    </div>
  );
}

render(<SizesDemo />);

Loading​


function LoadingDemo() {
  const [loading, setLoading] = React.useState(false);

  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Button loading>Saving…</Button>
      <Button variant="secondary" loading>
        Loading
      </Button>
      <Button
        variant="primary"
        loading={loading}
        onClick={() => {
          setLoading(true);
          setTimeout(() => setLoading(false), 2000);
        }}
      >
        Click to load
      </Button>
    </div>
  );
}

render(<LoadingDemo />);

Disabled​


function DisabledDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Button disabled>Disabled</Button>
      <Button variant="secondary" disabled>
        Disabled
      </Button>
    </div>
  );
}

render(<DisabledDemo />);

Toggle​


function ToggleButtonDemo() {
  const [pressed, setPressed] = useState(false);

  return (
    <Button variant={pressed ? 'primary' : 'secondary'} pressed={pressed} onPressedChange={setPressed}>
      {pressed ? 'On' : 'Off'}
    </Button>
  );
}

render(<ToggleButtonDemo />);

Icons​


function IconsDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Button startContent={<StarIconOutlinedRounded />}>Favorite</Button>
      <Button endContent={<ArrowRightIconOutlinedRounded />}>Continue</Button>
      <Button startContent={<StarIconOutlinedRounded />} endContent={<ArrowRightIconOutlinedRounded />}>
        Both
      </Button>
      <Button startContent={<StarIconOutlinedRounded />} rounded />
    </div>
  );
}

render(<IconsDemo />);

Accessibility & Keyboard​

  • Renders a native <button> element by default with correct type="button".
  • loading surfaces aria-busy="true" and aria-live="polite" for screen readers.
  • Toggle mode exposes aria-pressed reflecting the current state.
  • Disabled buttons set tabIndex={-1} and the native disabled attribute.
KeyBehavior
Enter / SpaceActivate the button.
TabMove focus to/from button.

API Reference​

Button​

See Spar Button docs for primitive behavior.

Props​

NameTypeDefaultDescription
childrenReact.ReactNode-Button label content.
variantButtonVariant'primary'Color variant.
appearanceButtonAppearance'filled'Visual appearance.
sizeButtonSize'base'Size scale.
roundedbooleanfalseRenders a pill-shaped (circular) button. Ideal for icon-only actions.
startContentReact.ReactNode-Content rendered before children — typically an icon, but accepts any node (spinner, badge, kbd, etc.). Wrapped in the content slot (there is no icon slot — target it via classNames.content / slotProps.content).
endContentReact.ReactNode-Content rendered after children. Same shape as startContent, also wrapped in the content slot.
loadingboolean-Whether the button shows a loading spinner.
pressedboolean-Whether the button is pressed. When defined, creates a toggle button.
classNamesPartial<Record<ButtonSlot, string>>-Per-slot extra classes.
slotPropsPartial<Record<ButtonSlot, React.HTMLAttributes<HTMLElement>>>-Per-slot HTML-attribute overrides.
classNamestring-Appends custom classes to the root slot of this part.

Events​

NameTypeDefaultDescription
onPressedChange(pressed: boolean) => void-Callback fired when toggle state changes

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-variantAlwaysReflects the resolved variant prop for theme recipe scoping.
data-typeAlwaysReflects the resolved appearance prop for theme recipe scoping.
data-sizeAlwaysReflects the resolved size prop for theme recipe scoping.
data-roundedWhen rounded is trueStyling hook for the pill-shaped state.
data-icon-onlyWhen icon-onlyAuto-detected when icons exist but no children.
data-loadingWhen loading is trueStyling hook for the loading state.
data-disabledWhen disabled is trueStyling hook for the disabled state.

Type Definitions​

NameDefinition
ButtonVariant'primary' | 'secondary' | 'neutral' | 'info' | 'success' | 'danger' | 'warning' | 'white' | 'black'
ButtonAppearance'filled' | 'filledLight' | 'outlined' | 'text'
ButtonSize'small' | 'base' | 'large'
ButtonSlot'root' | 'content' | 'label' | 'spinner'