# Button

A cross-platform React button component with primary/secondary variants, multiple color tones, sizes, loading states, and icon support. Works on both React (web) and React Native.
<!-- BEGIN:xui-mcp-instructions:button -->
An interactive element that triggers an action or event. Buttons communicate what will happen when the user activates them and are the primary way to drive decisions and actions across the product.

### When to use
- To trigger a primary action on a page or in a dialog (submit, save, confirm, proceed)
- To offer secondary or alternative actions alongside a primary one
- To navigate to a new state or screen when a link is semantically inappropriate
- When the action needs clear visual affordance — something clickable and intentional, not passive

### When not to use
- For toggling a binary on/off state — use a Switch
- For filtering or selecting options — use a ToggleButtonGroup, MultiSelect or Select instead
- Avoid using more than one Primary button in the same section — it dilutes hierarchy

### Content guidelines
- Labels should be short, action-oriented, and written in sentence case: *"Save changes"*, *"Delete account"*, not *"SAVE CHANGES"*.
- Use verbs that clearly describe the action: *"Create"*, *"Submit"*, *"Confirm"*, not vague labels like *"OK"* or *"Click here"*.
- For Alert tone (destructive actions), be explicit: *"Delete"*, *"Remove"*, *"Cancel subscription"* — not softened labels that hide the consequence.
- Loading state labels should have been descriptive before the state change — users already read the label before clicking, so hiding it during loading is acceptable.
- Keep labels under 3 words wherever possible. If the action needs explanation, add helper text nearby — not in the button.

### Behaviour guidelines
- One Primary button per view — hierarchy breaks when multiple high-emphasis buttons compete (Material Design, Carbon, Polaris all enforce this).
- Button width — buttons should be wide enough for their label plus padding; avoid stretching a button to fill a full container unless in a modal or mobile context.
- Destructive actions — always use Tone=Alert for irreversible operations. Consider adding a confirmation dialog before executing.
- Loading state — disable interaction immediately on click to prevent double-submission. Show the Loading state for the duration of the async operation, then return to Default (success) or surface an error.
- Disabled vs. hidden — prefer hiding a button over disabling it when the reason for unavailability is not obvious. If disabled, provide a tooltip or helper text explaining why.
- Icon-only buttons — if the label is removed entirely use Icon button component
- Touch targets — on mobile, the minimum touch target should be 44×44px. Sizes S (40px) and XS (32px) may need extra padding in touch contexts.
<!-- END:xui-mcp-instructions:button -->

## Installation

```bash
npm install @xsolla/xui-button
```

## Demo

### Basic Button

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";

export default function BasicButton() {
  return <Button onPress={() => console.log("Pressed!")}>Click me</Button>;
}
```

### Button Variants

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";

export default function ButtonVariants() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="tertiary">Tertiary</Button>
    </div>
  );
}
```

### Button Tones

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";

export default function ButtonTones() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <Button tone="brand">Brand</Button>
      <Button tone="brandExtra">Brand Extra</Button>
      <Button tone="alert">Alert</Button>
      <Button tone="mono">Mono</Button>
    </div>
  );
}
```

### Button Sizes

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";

export default function ButtonSizes() {
  return (
    <div style={{ display: "flex", gap: 16, alignItems: "center" }}>
      <Button size="xs">Extra Small</Button>
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
      <Button size="xl">Extra Large</Button>
    </div>
  );
}
```

### Button with Icons

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";
import { Plus, ChevronDown, FileDownloadOut } from "@xsolla/xui-icons-base";
import { ArrowRight } from "@xsolla/xui-icons";

export default function ButtonWithIcons() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <Button iconLeft={<Plus />}>Add Item</Button>
      <Button iconRight={<ArrowRight />}>Next Step</Button>
      <Button iconLeft={<FileDownloadOut />} iconRight={<ChevronDown />}>
        Download
      </Button>
    </div>
  );
}
```

### Loading Button

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";

export default function LoadingButton() {
  const [loading, setLoading] = React.useState(false);

  const handlePress = () => {
    setLoading(true);
    setTimeout(() => setLoading(false), 2000);
  };

  return (
    <Button loading={loading} onPress={handlePress}>
      {loading ? "Processing..." : "Submit"}
    </Button>
  );
}
```

## Anatomy

Import the component and use it directly:

```jsx
import { Button, IconButton, FlexButton, ButtonGroup } from '@xsolla/xui-button';
import { Plus } from '@xsolla/xui-icons-base';

// Basic button
<Button>Label</Button>

// Icon-only button
<IconButton icon={<Plus />} aria-label="Add item" />

// Flexible styling button
<FlexButton variant="brand">Label</FlexButton>

// Group of buttons
<ButtonGroup>
  <Button>First</Button>
  <Button>Second</Button>
</ButtonGroup>
```

## Examples

### Full Width Button

Use the `fullWidth` prop to make the button span the entire container width.

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";

export default function FullWidthButton() {
  return (
    <div style={{ width: 300 }}>
      <Button fullWidth>Full Width Button</Button>
    </div>
  );
}
```

### Disabled Button

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";

export default function DisabledButton() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <Button disabled>Disabled Primary</Button>
      <Button variant="secondary" disabled>
        Disabled Secondary
      </Button>
    </div>
  );
}
```

### Icon Button

<!-- BEGIN:xui-mcp-instructions:iconbutton -->
<!-- END:xui-mcp-instructions:iconbutton -->

```tsx
import * as React from "react";
import { IconButton } from "@xsolla/xui-button";
import { Plus, TrashCan } from "@xsolla/xui-icons-base";
import { Settings } from "@xsolla/xui-icons";

export default function IconButtonExample() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <IconButton icon={<Plus />} aria-label="Add" size="md" />
      <IconButton icon={<TrashCan />} aria-label="Delete" tone="alert" />
      <IconButton
        icon={<Settings />}
        aria-label="Settings"
        variant="secondary"
      />
    </div>
  );
}
```

### Button Group

<!-- BEGIN:xui-mcp-instructions:button-group -->
Use `ButtonGroup` to group related buttons together.
<!-- END:xui-mcp-instructions:button-group -->

```tsx
import * as React from "react";
import { Button, ButtonGroup } from "@xsolla/xui-button";

export default function ButtonGroupExample() {
  return (
    <ButtonGroup orientation="horizontal" size="md">
      <Button variant="secondary">Cancel</Button>
      <Button>Confirm</Button>
    </ButtonGroup>
  );
}
```

### Split Button Group

A horizontal group picks its layout from the child count: 3 or more buttons use
the split (`space-between`) layout — first button pinned to the left edge, the
rest grouped on the right — while 1–2 buttons stretch to fill the row.

Pass `split` to override that heuristic in either direction. It is the usual
choice for a wizard footer, where Back belongs on the left and Next on the
right. `split` has no effect on vertical groups or on a single button.

```tsx
import * as React from "react";
import { Button, ButtonGroup } from "@xsolla/xui-button";
import { ArrowRight } from "@xsolla/xui-icons-base";

export default function SplitButtonGroup() {
  return (
    <>
      {/* Two buttons, split apart instead of stretched */}
      <ButtonGroup split aria-label="Wizard navigation">
        <Button variant="secondary" tone="mono">
          Back
        </Button>
        <Button iconRight={<ArrowRight />}>Next</Button>
      </ButtonGroup>

      {/* Three buttons kept together instead of split */}
      <ButtonGroup split={false} aria-label="Grouped actions">
        <Button variant="secondary" tone="mono">
          Cancel
        </Button>
        <Button variant="secondary">Save Draft</Button>
        <Button>Submit</Button>
      </ButtonGroup>
    </>
  );
}
```

### Vertical Button Group

```tsx
import * as React from "react";
import { Button, ButtonGroup } from "@xsolla/xui-button";

export default function VerticalButtonGroup() {
  return (
    <ButtonGroup orientation="vertical" size="md">
      <Button fullWidth>Option 1</Button>
      <Button fullWidth>Option 2</Button>
      <Button fullWidth>Option 3</Button>
    </ButtonGroup>
  );
}
```

### Flex Button

<!-- BEGIN:xui-mcp-instructions:flexbutton -->
`FlexButton` provides more flexible styling options with different background modes.
<!-- END:xui-mcp-instructions:flexbutton -->

```tsx
import * as React from "react";
import { FlexButton } from "@xsolla/xui-button";
import { Link } from "@xsolla/xui-icons-base";

export default function FlexButtonExample() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <FlexButton variant="brand" background>
        With Background
      </FlexButton>
      <FlexButton variant="brand">Text Only</FlexButton>
      <FlexButton variant="brand" hoverBackground={false}>
        No Hover Fill
      </FlexButton>
      <FlexButton variant="tertiary" iconLeft={<Link />}>
        Link Style
      </FlexButton>
    </div>
  );
}
```

### Flex Button Without Hover Background

Set `hoverBackground={false}` for text-only actions that should keep a transparent background in hover and press states.

```tsx
import * as React from "react";
import { FlexButton } from "@xsolla/xui-button";

export default function FlexButtonWithoutHoverBackground() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <FlexButton variant="brand">Default Hover</FlexButton>
      <FlexButton variant="brand" hoverBackground={false}>
        No Hover Fill
      </FlexButton>
      <FlexButton variant="tertiary" hoverBackground={false}>
        Minimal Tertiary
      </FlexButton>
    </div>
  );
}
```

### Form Submit Button

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";

export default function FormSubmitButton() {
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        console.log("Form submitted");
      }}
    >
      <Button type="submit">Submit Form</Button>
    </form>
  );
}
```

### Button with Sublabel

Use the `sublabel` prop to add secondary text inline with the main label.

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";
import { Apple } from "@xsolla/xui-icons-brand";

export default function ButtonWithSublabel() {
  return (
    <Button iconLeft={<Apple />} sublabel="$39.99" labelAlignment="left">
      Buy Now
    </Button>
  );
}
```

### Button with Custom Content

Use `customContent` to add badges, tags, or other elements inside the button.

```tsx
import * as React from "react";
import { Button } from "@xsolla/xui-button";
import { Tag } from "@xsolla/xui-tag";
import { ArrowRight } from "@xsolla/xui-icons-base";

export default function ButtonWithCustomContent() {
  return (
    <Button iconRight={<ArrowRight />} customContent={<Tag size="sm">5x</Tag>}>
      Claim Reward
    </Button>
  );
}
```

### Tertiary Variant

The tertiary variant is a subtle filled button with a muted background and border. It is commonly used for secondary actions inside content cards (e.g. "Activate" in game cards).

```tsx
import * as React from "react";
import { Button, IconButton } from "@xsolla/xui-button";
import { Settings, ArrowRight } from "@xsolla/xui-icons-base";

export default function TertiaryButtons() {
  return (
    <div style={{ display: "flex", gap: 16 }}>
      <Button variant="tertiary" tone="brand" size="xs">
        Activate
      </Button>
      <Button
        variant="tertiary"
        tone="brand"
        size="xs"
        iconRight={<ArrowRight />}
      >
        Details
      </Button>
      <Button variant="tertiary">Learn More</Button>
      <IconButton
        variant="tertiary"
        icon={<Settings />}
        aria-label="Settings"
      />
    </div>
  );
}
```

## API Reference

### Button

The main button component. Renders a semantic `<button>` element.

**Button Props:**

| Prop             | Type                                                             | Default     | Description                                                                                                   |
| :--------------- | :--------------------------------------------------------------- | :---------- | :------------------------------------------------------------------------------------------------------------ |
| `testID`         | `string`                                                         | —           | Test ID for testing frameworks. On web this renders as `data-testid`; on React Native it renders as `testID`. |
| children         | `ReactNode`                                                      | -           | The button label content.                                                                                     |
| variant          | `"primary" \| "secondary" \| "tertiary" \| "ghost"`              | `"primary"` | The visual style variant.                                                                                     |
| tone             | `"brand" \| "brandExtra" \| "alert" \| "mono"`                   | `"brand"`   | The color tone of the button.                                                                                 |
| size             | `"xl" \| "lg" \| "md" \| "sm" \| "xs"`                           | `"md"`      | The size of the button.                                                                                       |
| disabled         | `boolean`                                                        | `false`     | Whether the button is disabled.                                                                               |
| loading          | `boolean`                                                        | `false`     | Whether to show loading spinner. Disables interaction when true.                                              |
| onPress          | `() => void`                                                     | -           | Callback fired when button is pressed.                                                                        |
| iconLeft         | `ReactNode`                                                      | -           | Icon to display on the left side.                                                                             |
| iconRight        | `ReactNode`                                                      | -           | Icon to display on the right side.                                                                            |
| sublabel         | `string`                                                         | -           | Secondary label text displayed inline with main label at 40% opacity.                                         |
| labelIcon        | `ReactNode`                                                      | -           | Small icon displayed directly next to the label text.                                                         |
| labelAlignment   | `"left" \| "center"`                                             | `"center"`  | Alignment of the label content within the button.                                                             |
| customContent    | `ReactNode`                                                      | -           | Custom content slot for badges, tags, or other elements.                                                      |
| fullWidth        | `boolean`                                                        | `false`     | Whether button should span full container width.                                                              |
| type             | `"button" \| "submit" \| "reset"`                                | `"button"`  | The HTML button type attribute.                                                                               |
| aria-label       | `string`                                                         | -           | Accessible label for the button.                                                                              |
| aria-describedby | `string`                                                         | -           | ID of element that describes the button.                                                                      |
| aria-expanded    | `boolean`                                                        | -           | Indicates if controlled element is expanded.                                                                  |
| aria-haspopup    | `boolean \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` | -           | Indicates popup type triggered by button.                                                                     |
| aria-pressed     | `boolean \| "mixed"`                                             | -           | Indicates pressed state for toggle buttons.                                                                   |
| aria-controls    | `string`                                                         | -           | ID of element controlled by this button.                                                                      |
| testID           | `string`                                                         | -           | Test identifier for testing frameworks.                                                                       |
| id               | `string`                                                         | -           | HTML id attribute.                                                                                            |

---

### IconButton

A button variant that displays only an icon. Requires `aria-label` for accessibility.

**IconButton Props:**

| Prop       | Type                                                | Default     | Description                                    |
| :--------- | :-------------------------------------------------- | :---------- | :--------------------------------------------- |
| icon       | `ReactNode`                                         | -           | **Required.** The icon to display.             |
| aria-label | `string`                                            | -           | **Required.** Accessible label for the button. |
| variant    | `"primary" \| "secondary" \| "tertiary" \| "ghost"` | `"primary"` | The visual style variant.                      |
| tone       | `"brand" \| "brandExtra" \| "alert" \| "mono"`      | `"brand"`   | The color tone of the button.                  |
| size       | `"xl" \| "lg" \| "md" \| "sm" \| "xs"`              | `"md"`      | The size of the button.                        |
| disabled   | `boolean`                                           | `false`     | Whether the button is disabled.                |
| loading    | `boolean`                                           | `false`     | Whether to show loading spinner.               |
| onPress    | `() => void`                                        | -           | Callback fired when button is pressed.         |
| type       | `"button" \| "submit" \| "reset"`                   | `"button"`  | The HTML button type attribute.                |

---

### FlexButton

A flexible button with more granular control over background, hover fills, and styling.

Forwards a `ref` to the underlying `<button>` element, and inherits `ThemeOverrideProps` (`themeMode`, `themeProductContext`).

**FlexButton Props:**

| Prop            | Type                                                                             | Default    | Description                                                    |
| :-------------- | :------------------------------------------------------------------------------- | :--------- | :------------------------------------------------------------- |
| children        | `ReactNode`                                                                      | -          | The button label content. Optional — omit for an icon-only button, which renders no text slot so the icon stays centred in the hit area. |
| variant         | `"brand" \| "primary" \| "secondary" \| "tertiary" \| "brandExtra" \| "inverse"` | `"brand"`  | The visual style variant.                                      |
| size            | `"xl" \| "lg" \| "md" \| "sm" \| "xs"`                                           | `"md"`     | The size of the button.                                        |
| background      | `boolean`                                                                        | `false`    | Whether to show background fill.                               |
| hoverBackground | `boolean`                                                                        | `true`     | Whether hover and press states should show a background color. |
| disabled        | `boolean`                                                                        | `false`    | Whether the button is disabled.                                |
| loading         | `boolean`                                                                        | `false`    | Whether to show loading spinner.                               |
| iconLeft        | `ReactNode`                                                                      | -          | Icon to display on the left side.                              |
| iconRight       | `ReactNode`                                                                      | -          | Icon to display on the right side.                             |
| onPress         | `() => void`                                                                     | -          | Callback fired when button is pressed.                         |
| type            | `"button" \| "submit" \| "reset"`                                                | `"button"` | The HTML button type attribute.                                |

**`getFlexButtonBoxSize(size?)`**

Returns the square side, in px, of an icon-only `FlexButton` — `20 | 22 | 28 | 32 | 36` for `xs`–`xl`, defaulting to `md`. Use it when a layout has to reserve a matching column for a button it does not render itself (as `Modal`'s header does), instead of hardcoding the number. `noPadding` does not change it; only the inner padding is removed.

```tsx
import { getFlexButtonBoxSize } from "@xsolla/xui-button";

<Box style={{ minWidth: getFlexButtonBoxSize("xl") }} />; // 36
```

---

### ButtonGroup

A container for grouping related buttons together.

**ButtonGroup Props:**

| Prop             | Type                                   | Default        | Description                                          |
| :--------------- | :------------------------------------- | :------------- | :--------------------------------------------------- |
| children         | `ReactNode`                            | -              | **Required.** Button children to group.              |
| orientation      | `"horizontal" \| "vertical"`           | `"horizontal"` | Layout direction of the buttons.                     |
| split            | `boolean`                              | -              | Force or suppress the split space-between layout.    |
| size             | `"xl" \| "lg" \| "md" \| "sm" \| "xs"` | `"md"`         | Size applied to the group spacing.                   |
| gap              | `number`                               | -              | Custom gap between buttons (overrides size default). |
| description      | `string`                               | -              | Description text shown below the group.              |
| error            | `string`                               | -              | Error message (replaces description when present).   |
| aria-label       | `string`                               | -              | Accessible label for the button group.               |
| aria-labelledby  | `string`                               | -              | ID of element labeling the group.                    |
| aria-describedby | `string`                               | -              | ID of element describing the group.                  |
| id               | `string`                               | -              | HTML id attribute.                                   |
| testID           | `string`                               | -              | Test identifier for testing frameworks.              |

`split` overrides the child-count heuristic that otherwise decides the
horizontal layout. Pass `split` to give a 2-button group the space-between
layout (the buttons keep their natural width rather than stretching to fill the
row), or `split={false}` to keep a 3+ button group together. It is ignored when
`orientation="vertical"` or when the group has fewer than 2 children.

**ButtonGroup Gap Defaults:**

| Size | Vertical Gap | Horizontal Gap |
| :--- | :----------- | :------------- |
| xl   | 16           | 16             |
| lg   | 16           | 16             |
| md   | 12           | 16             |
| sm   | 8            | 12             |
| xs   | 4            | 12             |

## Accessibility

- All buttons use semantic `<button>` elements
- `IconButton` requires `aria-label` for screen reader support
- `ButtonGroup` uses `role="group"` with proper ARIA attributes
- Focus indicators follow WCAG guidelines
- Disabled buttons are properly announced to assistive technology
- Loading state is communicated via `aria-busy` attribute
