# Button

A raised key that runs an action when pressed. Compose its label with ButtonText and an optional ButtonIcon.

![Five variants: default, secondary, outline, ghost and destructive, each a raised key over its edge.](https://ui.terratcg.app/previews/button/variants/ios-light.png)

```tsx
import { Button, ButtonText } from "@terratcg/ui";
import { View } from "react-native";

export default function ButtonVariants() {
  return (
    <View style={{ flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: 12 }}>
      <Button>
        <ButtonText>Default</ButtonText>
      </Button>
      <Button variant="secondary">
        <ButtonText>Secondary</ButtonText>
      </Button>
      <Button variant="outline">
        <ButtonText>Outline</ButtonText>
      </Button>
      <Button variant="ghost">
        <ButtonText>Ghost</ButtonText>
      </Button>
      <Button variant="destructive">
        <ButtonText>Delete</ButtonText>
      </Button>
    </View>
  );
}
```

## Import

```tsx
import { Button, ButtonIcon, ButtonText } from "@terratcg/ui";
```

## Usage

```tsx
<Button onPress={save}>
  <ButtonText>Save</ButtonText>
</Button>
```

A Button hugs its label; pass `block` to stretch it across its parent. Put text inside `ButtonText`: a bare string child does not render on React Native.

## Examples

### Sizes

Small, default and large keys: the label steps up with the key.

![Small, default and large keys: the label steps up with the key.](https://ui.terratcg.app/previews/button/sizes/ios-light.png)

```tsx
import { Button, ButtonText } from "@terratcg/ui";
import { View } from "react-native";

export default function ButtonSizes() {
  return (
    <View style={{ flexDirection: "row", flexWrap: "wrap", justifyContent: "center", alignItems: "center", gap: 12 }}>
      <Button size="sm">
        <ButtonText>Small</ButtonText>
      </Button>
      <Button>
        <ButtonText>Default</ButtonText>
      </Button>
      <Button size="lg">
        <ButtonText>Large</ButtonText>
      </Button>
    </View>
  );
}
```

### States

Loading holds the key's width with a spinner; disabled is molded flat; selected sits pressed in its well.

![Loading holds the key's width with a spinner; disabled is molded flat; selected sits pressed in its well.](https://ui.terratcg.app/previews/button/states/ios-light.png)

```tsx
import { Button, ButtonText } from "@terratcg/ui";
import { View } from "react-native";

export default function ButtonStates() {
  return (
    <View style={{ flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: 12 }}>
      <Button loading>
        <ButtonText>Saving</ButtonText>
      </Button>
      <Button disabled>
        <ButtonText>Disabled</ButtonText>
      </Button>
      <Button variant="outline" selected>
        <ButtonText>Selected</ButtonText>
      </Button>
    </View>
  );
}
```

### With an icon

ButtonIcon takes a render function: the key hands your icon its colour and size.

![ButtonIcon takes a render function: the key hands your icon its colour and size.](https://ui.terratcg.app/previews/button/with-icon/ios-light.png)

```tsx
import Add01Icon from "@hugeicons-pro/core-bulk-rounded/Add01Icon";
import Delete02Icon from "@hugeicons-pro/core-bulk-rounded/Delete02Icon";
import { HugeiconsIcon } from "@hugeicons/react-native";
import { Button, ButtonIcon, ButtonText } from "@terratcg/ui";
import { View } from "react-native";

export default function ButtonWithIcon() {
  return (
    <View style={{ flexDirection: "row", flexWrap: "wrap", justifyContent: "center", gap: 12 }}>
      <Button>
        <ButtonIcon>{({ color, size }) => <HugeiconsIcon icon={Add01Icon} color={color} size={size} />}</ButtonIcon>
        <ButtonText>Add</ButtonText>
      </Button>
      <Button variant="destructive">
        <ButtonIcon>{({ color, size }) => <HugeiconsIcon icon={Delete02Icon} color={color} size={size} />}</ButtonIcon>
        <ButtonText>Delete</ButtonText>
      </Button>
    </View>
  );
}
```

## Props

### Button

### ButtonProps

| Prop        | Type                                                                | Description                                                                                                                                                                                                                                                                                        |
| ----------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `variant?`  | `"default" \| "destructive" \| "ghost" \| "outline" \| "secondary"` | The key's look and weight: one `default` key per view, `secondary`, `outline` and `ghost` for the actions around it, `destructive` for what can't be undone. Default: `"default"`                                                                                                                  |
| `size?`     | `"default" \| "icon" \| "icon-lg" \| "icon-sm" \| "lg" \| "sm"`     | Height 36 (`sm`), 44 (`default`) or 52 (`lg`); the `icon` sizes are square faces of the same heights. Default: `"default"`                                                                                                                                                                         |
| `disabled?` | `boolean`                                                           | Inactive: readable, flat, no action and no feedback. Default: `false`                                                                                                                                                                                                                              |
| `loading?`  | `boolean`                                                           | Busy: keeps its size, shows a spinner over the label, blocks activation and exposes accessibilityState.busy. The host owns the operation. Default: `false`                                                                                                                                         |
| `selected?` | `boolean`                                                           | Selection cue (border, marker, weight) plus accessibilityState.selected. Omit for plain actions.                                                                                                                                                                                                   |
| `tint?`     | `string`                                                            | A hue the selection wears instead of the accent, for a choice that owns a colour (a label or a category): selected, the face is washed in it and its border and edge take it, with no leading marker: the hue is the cue. The label stays in ink. Ignored on the default and destructive variants. |
| `block?`    | `boolean`                                                           | Fill the parent's cross axis (alignSelf "stretch") instead of hugging the label. For stacked full-width actions such as a notice's column. Default: `false`                                                                                                                                        |
| `children?` | `ReactNode`                                                         | Compose ButtonText, ButtonIcon or host nodes.                                                                                                                                                                                                                                                      |
| `style?`    | `StyleProp<ViewStyle>`                                              | Outer style: layout, margins, width. Face colors come from variant and theme.                                                                                                                                                                                                                      |

Button also accepts React Native's `Pressable` props, such as `accessibilityLabel`, `hitSlop` and `testID`. `onPress` receives the native press event, and a ref points at the underlying `Pressable` view. `selected` and `tint` work together: `tint` only shows on a selected `secondary`, `outline` or `ghost` key.

### ButtonText

### ButtonTextProps

| Prop       | Type                                            | Description                                                                                                     |
| ---------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `weight?`  | `"bold" \| "medium" \| "regular" \| "semibold"` | Overrides the variant's default weight (`label`, `title` and `headline` are heavier than `body` and `caption`). |
| `tabular?` | `boolean`                                       | Tabular figures for counters and stats. Default: `false`                                                        |

ButtonText also accepts React Native's `Text` props, such as `numberOfLines`. Its size and colour come from the Button it sits in.

### ButtonIcon

### ButtonIconProps

| Prop       | Type                                                 | Description                                                               |
| ---------- | ---------------------------------------------------- | ------------------------------------------------------------------------- |
| `children` | `((slot: ButtonIconSlot) => ReactNode) \| ReactNode` | A host icon node, or a render function receiving the slot color and size. |

### useButtonContext

A label or icon of your own inside a Button reads the key it sits on with `useButtonContext()`: its `variant`, `size`, `foregroundColor`, `iconSize`, `textVariant` and its `disabled`, `selected` and `loading` states. Draw with `foregroundColor` and `iconSize` so your part matches ButtonText and ButtonIcon. It throws outside a Button.

## Accessibility

- **Role:** `button`, with `accessibilityState` keeping `disabled`, `busy` (while `loading`) and `selected` in sync. Your own `accessibilityState` is merged on top.
- **Label:** the ButtonText string. A key without text, such as an icon-only key, needs an `accessibilityLabel`.
- **Touch target:** at least 44 pt on iOS and 48 dp on Android. A smaller key keeps its face and grows its touch area with `hitSlop`.
- **Larger text:** under enlarged system text the label wraps and the face grows with it, so the action stays readable instead of clipping.
- **Reduce Motion:** the face no longer travels when pressed; the press tint stays.

## Platform differences

- **iOS and Android:** the same key, the same travel and no ripple. The minimum touch target follows each platform: 44 pt on iOS, 48 dp on Android.
- **Feedback:** a press calls ThemeProvider's `feedback` handler with `"press"`, or `"select"` when `selected` is set. TerraUI plays no haptic or sound itself; your handler decides.
- **Web:** Button renders through react-native-web for these docs, but the web is not a supported target.

## Rules

- Use `onPress`, never `onClick`, and style props, never `className`.
- Put text inside `ButtonText`; a bare string child does not render.
- Keep one `default` key per view. The actions around it step down to `secondary`, `outline` or `ghost`.
- Label the action with a verb and its object: "Delete playlist", not "OK".
- `loading` is yours to clear: the key stays busy until you set it back to `false`.
- Confirm a `destructive` action that can't be undone in a Dialog first.
