@xsolla/xui-button
v0.189.0
Published
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 trig
Readme
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.
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.
Installation
npm install @xsolla/xui-buttonDemo
Basic Button
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
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
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
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
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
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:
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.
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
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
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
Use ButtonGroup to group related buttons together.
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>
);
}Vertical Button Group
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
FlexButton provides more flexible styling options with different background modes.
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.
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
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.
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.
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).
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.
FlexButton Props:
| Prop | Type | Default | Description |
| :-------------- | :------------------------------------------------------------------------------- | :--------- | :------------------------------------------------------------- |
| children | ReactNode | - | The button label content. |
| 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. |
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. |
| 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. |
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 IconButtonrequiresaria-labelfor screen reader supportButtonGroupusesrole="group"with proper ARIA attributes- Focus indicators follow WCAG guidelines
- Disabled buttons are properly announced to assistive technology
- Loading state is communicated via
aria-busyattribute
