---
id: "packages/active-ui/button"
title: "Button"
description: "A button, or a link that looks like one, in six variants and eight sizes."
url: "/packages/active-ui/button"
locale: "en"
revision: "c4a59487a00e1ee9"
---

# Button

Use a button for an action on the current page: save, open a dialog, add a row. Use a link for
navigation to another page; when it must look like a button, give the link the button's classes
(below), not the button a link's role.

**Example: Variants**

Client: Runs entirely in your browser. Source: `examples/ui/button-variants.tsx`.

```tsx
// apps/docs/examples/ui/button-variants.tsx
'use client';

import {Button} from '@workspace/active-ui/components/primitives/button';

/** The six variants: one primary action, then decreasing emphasis. */
export default function ButtonVariants() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Button>Default</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="link">Link</Button>
    </div>
  );
}
```

## Usage

```tsx
import {Button} from '@workspace/active-ui/components/primitives/button';

<Button variant="outline">Export</Button>;
```

## Variants

`variant` sets the emphasis. Use one `default` button for the main action of a view, and lower
emphasis for the rest:

- `default`: the main action;
- `secondary`, `outline`: other actions;
- `ghost`: actions in toolbars and lists, where a border would add noise;
- `destructive`: an action that deletes or cannot be undone;
- `link`: an action that reads like text.

## Sizes and icons

`size` sets the height and padding. The `icon` sizes make a square button for a single icon.

**Example: Sizes and icons**

Client: Runs entirely in your browser. Source: `examples/ui/button-sizes.tsx`.

```tsx
// apps/docs/examples/ui/button-sizes.tsx
'use client';

import {ArrowRightIcon, PlusIcon} from 'lucide-react';

import {Button} from '@workspace/active-ui/components/primitives/button';

/**
 * Sizes, icons beside text, and icon-only buttons. An icon-only button has
 * no visible text, so it needs an accessible name of its own.
 */
export default function ButtonSizes() {
  return (
    <div className="flex flex-col gap-4">
      <div className="flex flex-wrap items-center gap-2">
        <Button size="xs">Extra small</Button>
        <Button size="sm">Small</Button>
        <Button>Default</Button>
        <Button size="lg">Large</Button>
      </div>
      <div className="flex flex-wrap items-center gap-2">
        <Button variant="outline">
          <PlusIcon data-icon="inline-start" aria-hidden />
          New scheme
        </Button>
        <Button variant="outline">
          Continue
          <ArrowRightIcon data-icon="inline-end" aria-hidden />
        </Button>
        <Button variant="outline" size="icon" aria-label="Add a row">
          <PlusIcon aria-hidden />
        </Button>
        <Button variant="outline" size="icon-sm" aria-label="Add a column">
          <PlusIcon aria-hidden />
        </Button>
      </div>
    </div>
  );
}
```

Mark an icon beside text with `data-icon="inline-start"` or `data-icon="inline-end"`: the button
then tightens its padding on that side. An icon-only button has no visible text, so it needs an
accessible name of its own, such as `aria-label`.

## States and links

**Example: Disabled, busy and as a link**

Client: Runs entirely in your browser. Source: `examples/ui/button-states.tsx`.

```tsx
// apps/docs/examples/ui/button-states.tsx
'use client';

import {useState} from 'react';

import {
  Button,
  buttonVariants,
} from '@workspace/active-ui/components/primitives/button';
import {Spinner} from '@workspace/active-ui/components/primitives/spinner';

/**
 * Disabled, busy, and a link that looks like a button. While busy, the
 * button is disabled, so a second click cannot start the action twice.
 */
export default function ButtonStates() {
  const [busy, setBusy] = useState(false);
  const save = () => {
    setBusy(true);
    window.setTimeout(() => setBusy(false), 1500);
  };
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Button disabled>Disabled</Button>
      <Button onClick={save} disabled={busy} data-busy={busy || undefined}>
        {busy ? <Spinner data-icon="inline-start" /> : null}
        {busy ? 'Saving…' : 'Save'}
      </Button>
      <a href="#api" className={buttonVariants({variant: 'outline'})}>
        A link, styled as a button
      </a>
    </div>
  );
}
```

- `disabled` removes the button from the tab order and from pointer input. Disable a button while
  its action runs, so a second click cannot start it twice, and show a
  `Spinner` beside the label.
- For a link, apply `buttonVariants()` to the link element itself. The Base UI button always
  carries `role="button"`, so rendering it as an `<a>` would hide the link from assistive
  technology.

## What your application provides

- The label, and an accessible name for an icon-only button.
- The busy state: the button shows what you pass; it does not track a pending action.
- `Spinner` is decorative unless you give it a `label`; inside a button whose text already says
  "Saving…", leave it decorative.

## Accessibility and keyboard

- A native `<button>`: `Enter` and `Space` activate it, `Tab` reaches it.
- Inside a form, a `Button` does not submit: it renders `type="button"` unless you pass another
  type. Give the button that sends the form `type="submit"`.
- The focus ring appears on keyboard focus only.

## API

### Button

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `size` | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "icon-xs" \| "lg" \| "sm" \| "xs" \| null` | no | The height and padding. `xs`, `sm`, `default` and `lg` for a button with text; `icon-xs`, `icon-sm`, `icon` and `icon-lg` for a square button with only an icon, which then needs an accessible name (`aria-label`). |
| `variant` | `"default" \| "destructive" \| "ghost" \| "link" \| "outline" \| "secondary" \| null` | no | The visual weight of the button. `default` for the main action of a screen or form; `outline` and `secondary` for other actions; `ghost` for a quiet action in a toolbar or a list; `destructive` for an action that deletes or cannot be undone; `link` for an action that reads as a link. Every variant but `link` sinks 1px while pressed; a link-styled action keeps its text and icon in place (`button-link-still`). |
| `focusableWhenDisabled` | `boolean` | no | Whether the button should be focusable when disabled. |
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). |
| `render` | `ComponentRenderFn<HTMLProps, ButtonState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**buttonVariants**: `(props?: (ConfigVariants<{ variant: { default: string; outline: string; secondary: string; ghost: string; destructive: string; link: string; }; size: { default: string; xs: string; sm: string; lg: string; icon: string; 'icon-xs': string; 'icon-sm': string; 'icon-lg': string; }; }> & ClassProp) \| undefined) => string`

`buttonVariants({variant, size})` returns the class names of a button, for an element that is not
a button.

> [!NOTE]
> **Based on shadcn/ui**
>
> This page adapts the Button documentation of shadcn/ui (MIT licence) to this
> package; see [Third-party notices](/third-party-notices).
