Skip to main content

Button

Atombeta@macaw-ds/ui · since 0.1.0

Triggers an action. Not navigation; for links use asChild with an anchor.

When to use

Any action the user takes: pay, export, save, void. A button changes something; it does not take the user somewhere.

When not to use

  • Navigation. Use an anchor or your router's Link, wrapped with asChild if you want button styling.
  • Toggles. A setting that stays on or off is a Switch (coming in a later release), not a button.
  • Menus. A button that only opens a menu belongs to Dropdown Menu (coming later), which handles the aria wiring.

Choosing a variant

VariantUse forPer view
primaryThe one action the screen exists forExactly one
secondaryAlternatives next to the primaryAny
ghostLow-emphasis actions in toolbars, table rows and dialogs' cancelAny
destructiveIrreversible actions, always behind a confirmationRare

Anatomy

An optional start icon, the label, an optional end icon. When loading is true the start icon becomes a spinner, the button is disabled, and the label stays where it was so nothing shifts.

Accessibility

  • The accessible name is the label. Icon-only buttons require aria-label, and TypeScript enforces it when iconOnly is set.
  • Enter and Space activate. Disabled and loading buttons leave the tab order.
  • loading sets aria-busy. The label is never replaced by the spinner alone.
  • The focus ring is the shared 2 px outline at 3:1 against every surface, including in forced-colors mode.

Do and don't

Do

  • Group a primary and a secondary button with gap-2, primary last in LTR layouts.

  • Use size="sm" ghost buttons for row actions in tables.

Don't

  • Do not pass className or style; use a variant or propose one in DECISIONS.md.

  • Do not place two primary buttons side by side.

  • Do not use loading for operations under 300 ms; it flickers.

Rules

Hard rules an agent must follow. The reviewer agent and the lint config check the ones that can be checked.

  1. One primary button per view. Everything else is secondary or ghost.
  2. Label is a verb phrase: "Pay invoice", not "OK" or "Submit".
  3. Icon-only buttons set iconOnly and aria-label.
  4. Destructive actions sit behind a confirmation step.
  5. Navigation uses asChild with an anchor; never a button with an onClick that routes.