# Button

**When to use:** Primary interactive action trigger. Supports loading state, icon, four styles in any colour, and sizes.

**Keywords:** cta, action button, ghost, text button, borderless, subtle, variant, danger, destructive, secondary

**Import:** `import { Button, ButtonStyleType, ButtonSize, ButtonType, ThemeColor } from '@ahrowe/ui'`

A button has a **style** (how much it stands out) and a **color**, set independently: any style
works with any colour. The colours are the library-wide `ThemeColor`s or any CSS colour, see
[ThemeColor.md](ThemeColor.md).

```tsx
import { Button, ButtonStyleType, ButtonSize, ButtonType, ThemeColor, useHotkeys } from '@ahrowe/ui';
import { faLocationArrow, faPhone, faPlus, faSave, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';

// The main action
<Button styleType={ButtonStyleType.Filled} onClick={handleSave}>Save</Button>

// Secondary: Outlined is the default style, Primary the default colour
<Button onClick={handleCancel}>Cancel</Button>
<Button color={ThemeColor.Neutral} onClick={handleCancel}>Cancel</Button>

// Destructive
<Button styleType={ButtonStyleType.Filled} color={ThemeColor.Error} onClick={handleDelete}>Delete</Button>

// A trash icon in a table row
<Button
  styleType={ButtonStyleType.Plain}
  color={ThemeColor.Error}
  size={ButtonSize.Icon}
  icon={faTrash}
  aria-label="Delete row"
  style={{ width: 32, height: 32 }}
/>

// A close button: Neutral takes the text colour of whatever it sits on
<Button
  styleType={ButtonStyleType.Plain}
  color={ThemeColor.Neutral}
  size={ButtonSize.Icon}
  icon={faXmark}
  aria-label="Close"
  style={{ width: 32, height: 32 }}
/>

// With icon + loading state
<Button styleType={ButtonStyleType.Filled} icon={faSave} isLoading={isSaving} onClick={handleSave}>
  Save
</Button>

// Small
<Button size={ButtonSize.Small}>Cancel</Button>

// A keyboard shortcut: the button draws it, useHotkeys binds it
useHotkeys({ 'mod+s': handleSave });
<Button styleType={ButtonStyleType.Filled} shortcut='mod+s' onClick={handleSave}>Save</Button>

// Press-and-hold: act on press rather than release
<Button
  icon={faPlus}
  aria-label="Increase"
  onPointerDown={startRepeat}
  onPointerUp={stopRepeat}
  onPointerCancel={stopRepeat}
/>

// Submit button in a form
<Button type={ButtonType.Submit} styleType={ButtonStyleType.Filled}>Submit</Button>

// A link: a long press copies the number, a middle click opens a new tab
<Button href="tel:+431234567" icon={faPhone}>Call</Button>
<Button href={mapsUrl} target="_blank" icon={faLocationArrow}>Directions</Button>
```

**Enums:**

- `ButtonStyleType`: `Filled` | `Light` | `Outlined` (default) | `Plain`
- `ThemeColor`: `Primary` (default) | `Neutral` | `Success` | `Info` | `Warn` | `Error`
- `ButtonSize`: `Small` | `Medium` (default) | `Large` | `Icon`
- `ButtonType`: `Button` | `Reset` | `Submit`

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `styleType` | `ButtonStyleType` | `Filled`: solid fill, for the one main action. `Light`: a tint of the colour behind text in the colour. `Outlined` (default): border and text in the colour. `Plain`: text in the colour, no fill or border, for toolbar, icon and close buttons |
| `color` | `ColorValue` | A `ThemeColor` (default `Primary`) or any CSS colour. `Neutral` takes the surrounding text colour, with a `--border-color` outline; `Error` is for destructive actions. See [ThemeColor.md](ThemeColor.md) |
| `size` | `ButtonSize` | Size variant. `Icon` strips all padding, inherits the surrounding text size, and centres a lone icon — set the width/height yourself |
| `type` | `ButtonType` | HTML button type, `Button` by default, unlike a bare `<button>`: in a form, only a `ButtonType.Submit` submits it. Not on a link |
| `href` | `string` | Renders an `<a>` instead of a `<button>`, see **As a link** |
| `icon` | `IconDefinition \| ReactElement` | FontAwesome icon or any React element, shown before label |
| `shortcut` | `string` | A combo in [useHotkeys](Hooks.md) spelling, e.g. `'mod+s'`. Shown after the label, written for the platform. Binding it is yours, see below |
| `isLoading` | `boolean` | Replaces content with a [Loader](Loader.md) in the button's text colour. Blocks clicks and a form submit like `disabled`, but keeps full colour and the keyboard focus. |
| `disabled` | `boolean` | Fades the button to half opacity |
| `onClick` | `(e: MouseEvent) => void \| Promise<void>` | |

Without an `href`, every native `<button>` attribute is accepted and forwarded — pointer handlers
(`onPointerDown` and friends, for press-and-hold controls that act on press rather than release),
`onContextMenu`, `tabIndex`, `name`, `value`, `form`, `autoFocus`, and the rest. `type` and
`onClick` are narrowed: `type` takes the `ButtonType` enum, and `onClick` may return a promise.
`color` is Button's own prop, not the obsolete HTML attribute. With an `href`, the native `<a>`
attributes take their place, see below.

## As a link

With an `href` a Button is an `<a>` that looks like a button, so it behaves like a link: a long
press offers to copy the address, and Ctrl/⌘-click and a middle click open a new tab. Use it for
anything that goes somewhere: `tel:`, `mailto:`, a maps URL, a page of your app. A button whose
`onClick` sets `window.location` gets none of that.

- **Types.** `ButtonProps` is two shapes, told apart by `href`. The link takes the native `<a>`
  attributes, `target`, `rel` and `download` among them, types `onClick` and `ref` for the `<a>`,
  and has no `type`. An `href` that may be `undefined` renders a `<button>` while it is, and an
  `onClick` beside it needs its event typed as `MouseEvent<HTMLElement>`.
- **Pages of your app** load the whole page unless `ConfigProvider` has a `navigate`, which then
  takes a plain click, see [ConfigProvider.md](ConfigProvider.md).
- **`disabled`** drops the `href`: an `<a>` cannot be disabled, so it leaves the tab order and is
  announced as a dimmed link. **`isLoading`** keeps it, and with it the focus, but is not followed.
- **`target="_blank"`** needs no `rel="noopener"`: every browser the library supports implies it.

**Shortcuts are drawn, not bound.** `shortcut` writes the combo in a pill after the label,
`⌘S` on an Apple platform and `Ctrl+S` elsewhere. It also sets `aria-keyshortcuts`, so a screen
reader announces it apart from the button's name. It is hidden on a touch screen
(`pointer: coarse`), which usually has no keyboard to press it on.

Listening for it is yours, with `useHotkeys` and the same string, because only your app knows
when the action is available. A button cannot tell: it would still fire while hidden in an
inactive tab, behind an open Modal, or twice when the same action has a button in the toolbar and
another in the footer. Turn the binding off wherever the button is disabled:
`useHotkeys({ 'mod+s': handleSave }, { enabled: !isSaving })`.

**Theming:** each colour reads the theme variables listed in [ThemeColor.md](ThemeColor.md). The
`Filled` `Primary` button also paints `--primary-gradient` when the theme sets one. See
[ThemeProvider.md](ThemeProvider.md).

## Custom colours and presets

`color` takes any CSS colour as well as a `ThemeColor`; the text on a `Filled` button is picked
for contrast. Presets are a component in your app, and a section or the whole app takes a default
through `ConfigProvider`. All three are in [ThemeColor.md](ThemeColor.md).

```tsx
<Button styleType={ButtonStyleType.Filled} color="var(--brand)">Upgrade</Button>
```

**States:** hover and press lay a tint of the button's own text colour over the whole button, so
every style, colour and theme gets them with no variables of their own. The keyboard focus ring
sits inside the edge, in `--focus-border-color`, or in the text colour on a `Filled` button. The
ripple is in the text colour too. Every button reserves a `1px` border, so switching style never
shifts layout or changes its height.

**Slots:** `background` `content` `loadingContainer` `label` `shortcut`

**Sizing in a flex row:** a Button never shrinks below its own content (`min-width: min-content`),
so an icon is never cut off by a flexible sibling. A multi-word label still wraps above that floor.

## Sizing

A Button is sized by its font. `--default-font-size` is the base, `Medium` is that size, and `Small` and `Large` are multiples of it (0.75x and 1.25x), so a theme with a smaller base font gets proportionally smaller controls everywhere. Padding and the icon gap are in `em`, so the whole box grows with the label rather than a large button wearing a medium button's padding.

`ButtonSize.Icon` is the exception: it inherits the surrounding text size and has no padding at all, because the caller sets the box.

## Migration from the `ButtonProps` interface

Before 0.51, `ButtonProps` and `ActionIconProps` were interfaces for a `<button>`. Each is now a
union of a button and a link shape, told apart by `href`. Code that renders a Button compiles
unchanged; a type built on one may not, because an interface cannot extend a union and `Omit` on
one keeps only what both shapes share:

| Before | After |
|--------|-------|
| `interface SaveButtonProps extends ButtonProps { … }` | `type SaveButtonProps = ButtonProps & { … }`, which keeps both shapes, so the wrapper takes an `href` too |
| `interface MyIconProps extends ActionIconProps { … }` | `type MyIconProps = ActionIconProps & { … }` |
| `Omit<ButtonProps, 'styleType'>` | `Omit<ButtonElementProps, 'styleType'>` for a wrapper that is only ever a `<button>` |

## Migration from `ButtonSize.Default`

Before 0.45, `ButtonSize.Default` was a second name for `Medium`, which is now the default. Replace
`size={ButtonSize.Default}` with `size={ButtonSize.Medium}`, or leave the prop out. The look does
not change.

## Migration from `Primary`, `Default` and `Delete`

Before 0.43, `styleType` set emphasis and colour in one value. `Primary`, `Default` and `Delete` no
longer exist, so TypeScript points at every place that names them:

| Before | After |
|--------|-------|
| `styleType={ButtonStyleType.Primary}` | `styleType={ButtonStyleType.Filled}` |
| `styleType={ButtonStyleType.Default}` | nothing: `Outlined` is the default |
| `styleType={ButtonStyleType.Delete}` | `styleType={ButtonStyleType.Filled} color={ThemeColor.Error}` |
| ActionButtons `submitButtonStyle={ButtonStyleType.Delete}` | `submitButtonColor={ThemeColor.Error}` |
| SplitButton `styleType={ButtonStyleType.Delete}` | `color={ThemeColor.Error}` (`Filled` is its default) |
| Card action `styleType: ButtonStyleType.Delete` | `styleType: ButtonStyleType.Filled, color: ThemeColor.Error` |
| `defaultProps={{ Button: { styleType: ButtonStyleType.Primary } }}` | `defaultProps={{ Button: { styleType: ButtonStyleType.Filled } }}` |

Two things compile unchanged and need a look of their own:

**A Button with no `styleType`.** It was transparent text in `--text-color` unless the theme set
`--button-default-*`; the built-in `defaultTheme` made it an outline in `--primary-color`, which
is now the default everywhere. To keep the old look where you want it, use
`styleType={ButtonStyleType.Plain} color={ThemeColor.Neutral}`.

**`--button-default-background`, `--button-default-color` and `--button-default-border` in a
theme.** They are gone, and since a theme accepts any variable they still compile and do nothing.
What they styled is now the default `color`, set once on `AhroweProvider`:

| Theme set | Instead |
|-----------|---------|
| `--button-default-color: var(--text-color)` and a `--border-color` border | `defaultProps={{ Button: { color: ThemeColor.Neutral } }}` |
| another colour | `defaultProps={{ Button: { color: 'var(--your-colour)' } }}` |
| a fill in `--button-default-background` | also `styleType: ButtonStyleType.Filled`; `ThemeColor.Neutral` fills with a mix of the text colour |

A default prop does not change with the theme. Where each theme had its own default button, give
each theme a variable of your own, `'--app-button': '…'`, and set
`defaultProps={{ Button: { color: 'var(--app-button)' } }}` once: the variable follows the theme.
