# Button

Triggers an action, such as saving a form or opening a dialog. For navigation, use [Link](/components/link.md), or [ButtonLink](#links) for a link that looks like a button.

```tsx
// demo.tsx
import {Button} from '@earendil-works/elanor-react';

export default function ButtonDemo() {
  return <Button>Save changes</Button>;
}
```

## Usage

```tsx
import {Button} from '@earendil-works/elanor-react';

<Button onClick={save}>Save changes</Button>;
```

Button renders `type="button"` by default. Set `type="submit"` to submit a form.

## Hierarchy

Use one primary button per view for the main action. Use secondary and tertiary for supporting actions.

```tsx
// hierarchy.tsx
import {Button, Stack} from '@earendil-works/elanor-react';

export default function ButtonHierarchy() {
  return (
    <Stack direction="row" gap="small" wrap="wrap">
      <Button>Primary</Button>
      <Button hierarchy="secondary">Secondary</Button>
      <Button hierarchy="tertiary">Tertiary</Button>
    </Stack>
  );
}
```

## Semantic

`negative` marks destructive actions; `positive` marks confirming ones. Combine with any hierarchy.

```tsx
// semantic.tsx
import {Button, Stack} from '@earendil-works/elanor-react';

export default function ButtonSemantic() {
  return (
    <Stack direction="row" gap="small" wrap="wrap">
      <Button semantic="negative">Delete project</Button>
      <Button semantic="positive">Approve request</Button>
    </Stack>
  );
}
```

## Density

Use `compact` in dense layouts, such as toolbars and table rows. The touch target keeps the default height.

```tsx
// density.tsx
import {Button, Stack} from '@earendil-works/elanor-react';

export default function ButtonDensity() {
  return (
    <Stack direction="row" gap="small" align="center" wrap="wrap">
      <Button>Default</Button>
      <Button density="compact">Compact</Button>
    </Stack>
  );
}
```

## Icons

`iconStart` and `iconEnd` add decorative icons; the label names the button. For an icon without a label, use [Icon button](/components/icon-button.md).

```tsx
// icons.tsx
import {AddIcon, ArrowForwardIcon} from '@earendil-works/elanor-icons-react';
import {Button, Stack} from '@earendil-works/elanor-react';

export default function ButtonIcons() {
  return (
    <Stack direction="row" gap="small" wrap="wrap">
      <Button iconStart={<AddIcon />}>New project</Button>
      <Button hierarchy="secondary" iconEnd={<ArrowForwardIcon />}>
        Continue
      </Button>
    </Stack>
  );
}
```

## Pending

`pending` disables the button while an action runs, keeps it focusable, and sets `aria-busy`.

```tsx
// pending.tsx
import {Button} from '@earendil-works/elanor-react';
import {useState} from 'react';

export default function ButtonPending() {
  const [pending, setPending] = useState(false);

  return (
    <Button
      pending={pending}
      onClick={() => {
        setPending(true);
        setTimeout(() => setPending(false), 2000);
      }}
    >
      {pending ? 'Saving…' : 'Save changes'}
    </Button>
  );
}
```

## Links

`ButtonLink` renders a plain `a` that looks like a Button, for navigation such as a call to action. It takes the same `hierarchy`, `semantic`, `density`, `iconStart`, and `iconEnd`, and keeps link semantics: screen readers announce it as a link, and Enter follows it. Don't pass a link to Button's `render`; that gives the `a` a button role.

```tsx
// link.tsx
import {ArrowForwardIcon} from '@earendil-works/elanor-icons-react';
import {ButtonLink, Stack} from '@earendil-works/elanor-react';

export default function ButtonLinkDemo() {
  return (
    <Stack direction="row" gap="small" wrap="wrap">
      <ButtonLink href="#" iconEnd={<ArrowForwardIcon />}>
        Open dashboard
      </ButtonLink>
      <ButtonLink href="#" hierarchy="secondary">
        Read the guide
      </ButtonLink>
    </Stack>
  );
}
```

ButtonLink passes its props and ref through to the `a`, so a router can wrap it, as with TanStack Router's `createLink`.

A link can't be disabled or pending. When a destination isn't available, render a Button or text instead.

## Disabled

```tsx
// disabled.tsx
import {Button} from '@earendil-works/elanor-react';

export default function ButtonDisabled() {
  return <Button disabled>Save changes</Button>;
}
```

## API

### `Button`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hierarchy` | `'primary' \| 'secondary' \| 'tertiary'` | `'primary'` | Visual emphasis. Use one `primary` button per view. |
| `semantic` | `'neutral' \| 'negative' \| 'positive'` | `'neutral'` | `negative` for destructive actions, `positive` for confirming ones. |
| `density` | `'compact' \| 'default'` | `'default'` | `compact` removes block padding but keeps the touch target height. |
| `iconStart` | `ReactNode` | - | Decorative icon before the label. |
| `iconEnd` | `ReactNode` | - | Decorative icon after the label. |
| `pending` | `boolean` | `false` | Disables the button while an action runs, keeps it focusable, and sets `aria-busy`. |
| `type` | `'submit' \| 'reset' \| 'button'` | `'button'` |  |

Also accepts Base UI [`Button`](https://base-ui.com/react/components/button.md) props.

### `ButtonLink`

A link that looks like a Button, for navigation such as a call to action. Renders a plain `a` without a button role, and passes its props and ref through, so a router can wrap it.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `hierarchy` | `'primary' \| 'secondary' \| 'tertiary'` | `'primary'` | Visual emphasis. Use one `primary` button per view. |
| `semantic` | `'neutral' \| 'negative' \| 'positive'` | `'neutral'` | `negative` for destructive actions, `positive` for confirming ones. |
| `density` | `'compact' \| 'default'` | `'default'` | `compact` removes block padding but keeps the touch target height. |
| `iconStart` | `ReactNode` | - | Decorative icon before the label. |
| `iconEnd` | `ReactNode` | - | Decorative icon after the label. |

Renders `<a>`. Also accepts its HTML attributes and `ref`.
