Source: https://monarchui.dev/components/button/

Components · Actions

# Button

Primary, secondary, ghost and danger actions; loading and icon-only forms.

@rookcom/monarch-react

Component

## Variants

## Usage examples

### Sizes, icons, loading

### As a link

### Status and brand

## Purpose

Triggers an action in place: saving, submitting, opening a dialog or menu. IconButton is the square, icon-only form.

### Use when

- The user performs an action on the current view (save, delete, open a menu).
- A link must look like a button, for example a primary "New ticket" call to action (use `asChild`).

### Avoid when

- The control navigates as ordinary text: use Link.
- Status is only being shown, not acted on: use Badge. `success` and `warning` buttons are for actions whose outcome is that status ("Approve", "Mark at risk").
- The choice is on/off: use Switch or Checkbox.

## Design principles

- One `primary` button per view region; everything else is `secondary` or `ghost`.
- `danger` is for the final, destructive confirmation; `danger-subtle` for destructive actions that still need confirming.
- Labels are verbs that say what happens ("Save changes", not "OK").
- Never submits by accident: the default `type` is `button`.
- `success` and `warning` are status actions, used sparingly next to a neutral alternative.
- `brand` is for a tenant or customer colour (a hosted booking page, a public form). Contrast is guaranteed by the tokens engine, never by the caller.

## API reference

```tsx
import { Button, IconButton } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `Button` | component | client | `.mn-theme` |
| `IconButton` | component | client | `.mn-theme` |

Both components forward their ref to the underlying `<button>` (or the child element with `asChild`).

All native button attributes pass through.

Generated from the TypeScript declarations of `packages/react/src/components/actions/button`.

## Properties

### `Button`

The primary action control. Defaults to `type="button"` so a button inside a form never submits by accident; pass `type="submit"` explicitly.

| Prop | Type | Description |
| --- | --- | --- |
| `asChild` | `boolean` | Render the child element (e.g. a Next.js <Link>) with button styling. |
| `block` | `boolean` | Stretch to the container width. |
| `brandColor` | `string` | Brand or tenant colour for `variant="brand"`, as `#rrggbb` or `#rgb`. The fill and text are derived per theme by the tokens contrast engine, so the label always meets WCAG AA (7:1 in more-contrast themes); the colour may be darkened or lightened to get there. Ignored by other variants. An invalid or missing colour falls back to the product accent. |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `disabled` | `boolean` |  |
| `id` | `string` |  |
| `leadingIcon` | `ReactNode` | Icon before the label. Decorative: hidden from assistive technology. |
| `loading` | `boolean` | Shows a spinner, sets aria-busy and blocks clicks while keeping focus. |
| `size` | `ButtonSize` |  |
| `trailingIcon` | `ReactNode` |  |
| `variant` | `ButtonVariant` | Visual weight. Use one `primary` per view region. Default `secondary`. |

### `IconButton`

Square button containing only an icon. `label` is required by the type system.

| Prop | Type | Description |
| --- | --- | --- |
| `icon` required | `ReactNode` |  |
| `label` required | `string` | Required accessible name, since there is no visible text. |
| `asChild` | `boolean` | Render the child element (e.g. a Next.js <Link>) with button styling. |
| `brandColor` | `string` | Brand or tenant colour for `variant="brand"`, as `#rrggbb` or `#rgb`. The fill and text are derived per theme by the tokens contrast engine, so the label always meets WCAG AA (7:1 in more-contrast themes); the colour may be darkened or lightened to get there. Ignored by other variants. An invalid or missing colour falls back to the product accent. |
| `className` | `string` |  |
| `disabled` | `boolean` |  |
| `id` | `string` |  |
| `loading` | `boolean` | Shows a spinner, sets aria-busy and blocks clicks while keeping focus. |
| `size` | `ButtonSize` |  |
| `variant` | `ButtonVariant` | Visual weight. Use one `primary` per view region. Default `secondary`. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `variant` | `'primary'``'secondary'``'ghost'``'danger'``'danger-subtle'``'success'``'warning'``'brand'` | Visual weight. Default `secondary`; IconButton defaults to `ghost`. Exposed as `data-variant`. `success` and `warning` use the status tokens; `brand` uses `brandColor`. |
| `brandColor` | `'#rrggbb'``'#rgb'` | The tenant colour for `variant="brand"`. Fill, hover, active and text are derived for light, dark and both more-contrast themes with `deriveIntent` from @rookcom/monarch-tokens, so text meets 4.5:1 (7:1 in more contrast). Sets `data-brand` and inline `--_brand-*` properties. Missing or invalid: the product accent is used. |
| `size` | `'xs'``'sm'``'md'``'lg'` | Control height from the `--mn-control-*` tokens. Default `md`. Exposed as `data-size`. |
| `block` | `'true'``'false'` | Stretches to the container width (`data-block`). |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Hover and active | – | Background steps through the variant’s hover and active tokens. |
| Focus | – | Visible focus ring from `--mn-color-focus-ring` on `:focus-visible` only. |
| Disabled | `disabled, aria-disabled="true"` | Native `disabled` plus `aria-disabled`; 50% opacity and a not-allowed cursor. |
| Loading | `data-loading, aria-busy="true", aria-disabled="true"` | Replaces the leading icon with a small Spinner, blocks clicks, and stays focusable so keyboard users keep their place. |

## Interaction behaviour

- Click, Enter and Space call `onClick` (native button behaviour).
- While `disabled` or `loading`, clicks are prevented and `onClick` is not called.
- With `asChild`, the child element (for example a router link) receives the classes, data attributes and handlers; no `type` attribute is added.
- `leadingIcon` and `trailingIcon` render in decorative spans.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves focus to the button, including while it is loading. |
| Enter / Space | Activates the button. |

## Accessibility

Role

button (or the child element’s role with asChild)

Accessible name

The visible label; IconButton requires

label

, which becomes

aria-label

.

- Icons are wrapped in `aria-hidden` spans, so they never add to the accessible name.
- Loading sets `aria-busy="true"` and `aria-disabled="true"` instead of `disabled`, so focus is not lost.
- IconButton cannot be rendered without a `label`: the type requires it.
- `brand` text contrast is computed, not assumed: a light brand colour gets dark text, a mid colour is darkened until white text passes.

## Responsive behaviour

- Heights come from the `--mn-control-*` tokens and do not change with viewport width.
- `block` makes the button fill narrow containers such as mobile dialog footers.
- Labels do not wrap (`white-space: nowrap`).

## Theme support

- Colours are Monarch semantic tokens only, so light, dark and more-contrast themes apply automatically.
- `primary` uses the product accent (`--mn-color-accent-solid`), so each Rook product gets its own accent with no code change.
- `danger` variants use the danger tokens, which do not change with the product accent; `success` and `warning` likewise use their status tokens.
- `brand` carries one derived colour set per theme and picks the one matching `.dark` / `data-mn-theme` / `data-mn-contrast` on an ancestor, so a tenant colour also adapts to dark and more-contrast themes.

## Do and don't

### Do

- Use one primary button per region, placed where the eye ends (the right of a dialog footer).
- Keep the button focusable while work is in progress: use `loading`, not `disabled`.
- Use `asChild` to style a router link as a button instead of nesting a button in a link.

### Don't

- Do not put several primary buttons side by side.
- Do not use an IconButton for an action whose icon is ambiguous; give it a visible label instead.
- Do not use `danger` for actions that are reversible.
- Do not pass a brand colour through `style` or `className`: use `variant="brand"` + `brandColor` so contrast is checked.

## Related components

- [Link](https://monarchui.dev/components/link/): Navigation that reads as text.
- [DropdownMenu](https://monarchui.dev/components/dropdown-menu/): Several related actions behind one button.
- [Spinner](https://monarchui.dev/components/spinner/): Loading indicator the button uses internally.
- [Toolbar](https://monarchui.dev/components/toolbar/): Groups buttons with arrow-key navigation.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `@radix-ui/react-slot``@rookcom/monarch-tokens``react` |
| Monarch components | [Spinner](https://monarchui.dev/components/spinner/) |

## Testing coverage

- Defaults to `type="button"`.
- Activates with click, Enter and Space.
- Stays focusable but inert while loading (aria-busy, aria-disabled, no click).
- Disables natively when `disabled`.
- Renders the child element with `asChild`, without a `type` attribute.
- Keeps the child element and spinner with `asChild` + `loading`, and both icons with `asChild`.
- Hides decorative icons from assistive technology.
- IconButton uses `label` as its accessible name and shows only the spinner while loading.
- `success` and `warning` expose their variant; `xs` its size.
- `brand` derives a fill and text for all four themes that meet 4.5:1 (7:1 in more contrast) for light, mid and dark brand colours, accepts `#rgb`, keeps caller styles, and falls back to the accent for an invalid colour.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery; Cross-browser axe: /components/button |

Tests: `packages/react/src/components/actions/button`

## All example code

### Variants

```tsx
import { Button } from '@rookcom/monarch-react';

<Button variant="primary">Save changes</Button>
<Button>Cancel</Button>
<Button variant="ghost">Learn more</Button>
<Button variant="danger">Delete</Button>
<Button variant="danger-subtle">Remove</Button>
```

### Sizes, icons, loading

```tsx
import { Button, IconButton } from '@rookcom/monarch-react';

// MoreIcon, PlusIcon: demo icons from the docs site; use your own.

<Button size="xs">Extra small</Button>
<Button size="sm">Small</Button>
<Button size="md" leadingIcon={<PlusIcon />}>
  New ticket
</Button>
<Button size="lg" variant="primary" loading>
  Saving
</Button>
<Button disabled>Disabled</Button>
<IconButton label="More actions" icon={<MoreIcon />} />
```

### As a link

```tsx
import { Button } from '@rookcom/monarch-react';

<Button asChild variant="primary">
  <a href="#/components/button">Open ticket</a>
</Button>
```

### Status and brand

```tsx
import { Button } from '@rookcom/monarch-react';

<Button variant="success">Approve</Button>
<Button variant="warning">Mark at risk</Button>
<Button variant="brand" brandColor="#0f766e">
  Book a call
</Button>
<Button variant="brand" brandColor="#facc15">
  Join waitlist
</Button>
<Button variant="success" size="xs">
  Approve
</Button>
```
