---
title: Button
description: Runs an action. Five variants, four sizes and three shapes.
---

```tsx
import { Button } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function ButtonVariants() {
  return (
    <Preview>
      <Button.Root>Default</Button.Root>
      <Button.Root variant="field">Field</Button.Root>
      <Button.Root variant="primary">Primary</Button.Root>
      <Button.Root variant="ghost">Ghost</Button.Root>
      <Button.Root variant="danger">Danger</Button.Root>
    </Preview>
  );
}
```

- One `primary` per view, for the main action on screen.
- No outlined buttons. Use `ghost` when a fill is too loud.
- `danger` only confirms a destructive action, such as Delete in its confirmation dialog. The button that opens that dialog stays `default` or `ghost`.
- Labels are verbs in sentence case: "Resolve", "Create issue".
- Icons sit before the label; a chevron that opens a menu sits after it.

| Variant   | Use                                            |
| --------- | ---------------------------------------------- |
| `default` | Header actions: filters, period                |
| `field`   | Actions inside a [List](/deft/components/list) |
| `primary` | The one main action on screen                  |
| `ghost`   | Toolbar, row and menu actions                  |
| `danger`  | Confirms a destructive action                  |

## Sizes

```tsx
import { Button } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function ButtonSizes() {
  return (
    <Preview>
      <Button.Root>Medium</Button.Root>
      <Button.Root size="sm">Small</Button.Root>
      <Button.Root size="xs">Extra small</Button.Root>
      <p>
        Text with an <Button.Root size="inline">inline</Button.Root> button.
      </p>
    </Preview>
  );
}
```

`md` is the default, for panels and forms. `sm` goes in toolbars and the action bar, `xs` for compact buttons inside a field or a code frame. `inline` has no fixed height: it takes the font size and line height of the text around it, for a control inside a sentence.

## Shapes

```tsx
import { Button } from "@openperf/deft";
import { IconPlus } from "@tabler/icons-react";

import { Preview } from "../../src/Preview";

export default function ButtonShapes() {
  return (
    <Preview>
      <Button.Root>Rect</Button.Root>
      <Button.Root shape="pill">Pill</Button.Root>
      <Button.Root aria-label="Add" shape="square">
        <IconPlus />
      </Button.Root>
    </Preview>
  );
}
```

`square` is for icon-only buttons and needs an `aria-label`. `pill` is for buttons on a pill surface, such as the [action bar](/deft/components/action-bar).

## Loading

```tsx
import { Button } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function ButtonLoading() {
  return (
    <Preview>
      <Button.Root loading={true}>Loading</Button.Root>
      <Button.Root
        onClick={() =>
          new Promise((resolve) => {
            setTimeout(resolve, 1500);
          })
        }
      >
        Click to load
      </Button.Root>
      <Button.Root disabled={true}>Disabled</Button.Root>
    </Preview>
  );
}
```

An `onClick` that returns a promise shows the spinner until it settles. `loading` shows it on demand. While loading, the button is disabled and sets `aria-busy`.

## API

| Prop | Type | Default | Description |
| - | - | - | - |
| `variant?` | `"default" \| "field" \| "primary" \| "ghost" \| "danger"` | `"default"` | |
| `size?` | `"inline" \| "xs" \| "sm" \| "md"` | `"md"` | |
| `shape?` | `"rect" \| "pill" \| "square"` | `"rect"` | |
| `loading?` | `boolean` | - | Shows a spinner and blocks clicks. |
| `onClick?` | `(event) => unknown` | - | A returned promise shows the spinner until it settles. |
| `render?` | `ReactElement \| function` | - | Renders another element, such as a router link, with the button's styles. |

Other props go to the native `button`. Built on Base UI [Button](https://base-ui.com/react/components/button).
