A button asks for one thing. Its label says what it does, its variant says how important it
is, its colour says what it is about, and its state says whether it can be pressed at all.
Nothing about it is bespoke: the radius comes from the application's configuration unless
the instance states one, and a busy button keeps its width, so the row it sits in does not
jump while a request is in flight.

It renders a real `<button>`, or an `<a>` when it navigates, the variant decides the look,
not the element.

```tsx demo="forms/button-preview"
```

## Usage

```tsx
import { Button } from "@asheeui/web";

<Button onClick={() => save(invoice)}>Save invoice</Button>
```

An action that navigates takes `href` instead of `onClick`:

```tsx
<Button href="/invoices">All invoices</Button>
```

## Variants

Five treatments. `solid` is the primary action on a screen; `bordered` and `faded` are
secondary; `ghost` belongs in a toolbar or a footer where the boundary would be noise; and
`underlined` is for an action that reads as a link inside prose.

```tsx demo="forms/button-variants"
```

`underlined` is the one variant that overrides radius: an underlined control with rounded
corners is two vocabularies at once, so the radius resolves to `none`.

## Colours

Colour is a role, not a hex value: `primary`, `secondary`, `success`, `warning` and `danger`. A
custom theme changes what a role *looks like*, not which roles exist, the set is closed, and a
colour name outside it is a configuration warning rather than a new role. The same five on the
secondary treatment read as a lighter set, which is what a page uses for a row of equivalent
actions.

```tsx demo="forms/button-colours"
```

## Sizes and width

Density is the same scale the fields use, so a button and an input in one row line up. An
icon-only button keeps the square footprint of its size, and `fullWidth` makes the action
fill its column, useful at the foot of a form or inside a card.

```tsx demo="forms/button-sizes"
```

## Radius

Radius resolves from the application's `defaultRadius` unless the instance states one, so a
product that rounds everything to `full` states it once.

```tsx demo="forms/button-radius"
```

## States

A button is available, unavailable or busy. `isDisabled` marks it unavailable; `isLoading`
marks it busy, and a busy button is also unavailable, pressing it twice is the mistake the
state exists to prevent.

```tsx demo="forms/button-states"
```

Both states keep the label: a control that changes its text while it works moves everything
around it. The state is announced instead, through `aria-disabled` and `aria-busy`.

## Content

`startContent` and `endContent` take a node either side of the label, an icon, a glyph, a
count. `icon` switches to the compact layout, which is square and has no visible label, so
it needs one for assistive technology.

```tsx demo="forms/button-content"
```

## As a link

`href` renders an anchor with the button's classes, so it can be opened in a new tab,
copied as a URL and announced as a link. The `link` prop does the same for a framework's own
router component, pass its component and props and AsheeUI substitutes it:

```tsx
import { Link } from "@tanstack/react-router";

<Button
  link={{
    component: Link,
    props: { to: "/invoices" },
  }}
>
  All invoices
</Button>
```

```tsx demo="forms/button-link"
```

## Props

```props Button
```

The defaults are worth reading as a cascade rather than a list. `variant` and `colour` are
intentionally **not** registered by the component: they come from the application's
`defaultVariant` and `defaultColor`. `size` is registered as `md`. `radius` resolves from
the application's `defaultRadius`. So a page with no configuration gets a `faded`,
`primary`, `md` button with an `xs` radius, the framework's deliberate boxy default, and
the same page with two lines of configuration gets the product's button everywhere, without
restating it.

## Accessibility

- The element is a real `<button type="button">`, so <kbd>Enter</kbd> and <kbd>Space</kbd>
  activate it, it sits in the tab order without a `tabindex`, and a form does not submit
  when it is pressed.
- `isDisabled` sets `aria-disabled` and cancels the press in the handler. The control stays
  focusable and findable: a disabled action that disappears from the tab order is a control
  the reader cannot discover.
- `isLoading` sets `aria-busy` and disables interaction the same way. The label does not
  change, so the announcement says what is happening rather than what replaced it.
- The focus ring is drawn with `focus-visible`, so it appears for a keyboard and not for a
  mouse.
- An icon-only button has no visible text, so it must be given `aria-label`.
- When `href` or `link` is used the element is an anchor and announces as a link: an action
  that navigates should never look like one that does not.

## Native

`@asheeui/native` ships the same `Button` with the same props and the same cascade. The
platform's differences are the platform's: the control is the native pressable, the press
state is the device's, and `href` follows the platform's URL handler instead of rendering
an anchor. See [React Native](/platforms/native).

## Related

```related link, form, tooltip
```
