Button
Button triggers an action when clicked. Use it for form submissions, confirmations, navigation, or any interaction that needs a clear call to action.
Installation
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0
Requires React 19+, base CSS and a Theme provider. See Setup.
Button
Button triggers an action when clicked. Use it for form submissions, confirmations, navigation, or any interaction that needs a clear call to action.
| Prop | Type | Default | Description |
|---|---|---|---|
| label * | string | Accessible label. Rendered as visible text by default; used as aria-label when isIconOnly is true. | |
| variant | 'primary' | 'secondary' | 'ghost' | 'destructive' | 'secondary' | Visual style variant. |
| size | 'sm' | 'md' | 'lg' | 'md' | Size variant. |
| elevation | 'none' | 'low' | 'med' | 'high' | 'none' | Resting shadow depth for floating buttons (e.g. a FAB). none is the default flat button; low/med/high map to the shadow token scale. Ignored inside a ButtonGroup, where elevation is owned by the group. |
| type | 'button' | 'submit' | 'reset' | 'button' | HTML button type attribute. |
| name | string | HTML name attribute for form submission. | |
| value | string | number | readonly string[] | HTML value attribute for form submission. | |
| form | string | Associates the button with a form element by ID. | |
| isLoading | boolean | false | Shows a loading spinner and disables interaction. Announces "Loading" via a live region. |
| isInterruptible | boolean | false | Keep the button clickable while a clickAction is pending: the spinner and aria-busy still show, but the button is not disabled and the action is not deduped, so a re-click lands and interrupts the in-flight action with a fresh one. |
| isDisabled | boolean | false | Disables the button. When a tooltip is present, uses aria-disabled instead of native disabled so the button stays focusable. |
| icon | ReactNode | Icon element rendered before the label text. An Astryx Icon with no explicit size defaults to sm for sm/md buttons and md for lg buttons. | |
| isIconOnly | boolean | false | When true, renders as a square icon-only button with label as aria-label. Requires icon. Tip: for a dedicated icon-only button component, use IconButton from '@astryxdesign/core/IconButton' instead. |
| width | SizeValue | Width of the button. Numbers are treated as pixels, strings are used as-is (e.g., '100%' for a full-width button). By default the button sizes to its content, never grows past its container, and truncates its label with an ellipsis when a row is too narrow; icon-only buttons stay square. | |
| children | ReactNode | Optional override for visible text. When provided, displayed instead of label, but label is still required (it provides the accessible name). For most cases, just use label alone: <Button label="Save" />. | |
| endContent | ReactElement<IconProps> | ReactElement<BadgeProps> | Trailing icon or badge rendered after the label. Ignored when isIconOnly is true. Color is inherited from the button variant. | |
| tooltip | string | Tooltip text shown on hover. | |
| onClick | (e: MouseEvent) => void | Standard click handler (passed through from ButtonHTMLAttributes). | |
| clickAction | (e: MouseEvent) => void | Promise<void> | Async click handler. Shows loading state while the returned promise is pending. | |
| href | string | When provided, renders the button as a link element (<a> or custom link component). The destination follows the shared navigation rule described on the Link href prop. | |
| as | ComponentType | Custom link component to use when href is provided (e.g. Next.js Link). | |
| target | string | HTML target attribute when rendered as a link (e.g. "_blank"). | |
| rel | string | HTML rel attribute when rendered as a link (e.g. "noopener noreferrer"). |
Usage guidance
- Reserve primary for the single most important action in the view. Use secondary or ghost for everything else based on emphasis.
- Write labels that describe the action ("Save changes", "Delete account", "Send invite"), not vague labels like "OK" or "Click here".
- Show a loading state for actions that take time, like saving or submitting, so the user knows it is working.
- Always provide a label for icon-only buttons so screen readers can announce what the button does. Add a tooltip for sighted users.
- For a dedicated icon-only button, use IconButton from '@astryxdesign/core/IconButton'. It is a separate component, not exported from '@astryxdesign/core/Button'.
- Avoid: Place more than one primary button in the same view; this dilutes the visual hierarchy.
- Avoid: Use the destructive variant without a confirmation step for irreversible actions like deleting data.
- Avoid: Use a button for navigation. If it only takes the user to another page, use a link instead. Buttons are for actions like saving, deleting, or submitting.
Actions in narrow rows
import {useState} from 'react';
import {Button} from '@astryxdesign/core/Button';
import {Card} from '@astryxdesign/core/Card';
import {Heading} from '@astryxdesign/core/Heading';
import {HStack} from '@astryxdesign/core/HStack';
import {Icon} from '@astryxdesign/core/Icon';
import {IconButton} from '@astryxdesign/core/IconButton';
import {StackItem} from '@astryxdesign/core/Stack';
import {TextInput} from '@astryxdesign/core/TextInput';
import {VStack} from '@astryxdesign/core/VStack';
// On a phone (or any card narrower than its actions), a labelled Button
// shrinks with its row and truncates its label with an ellipsis instead of
// overflowing or being clipped by the card. The full label stays the
// accessible name. Icon-only buttons keep their square size.
function ReviewersCard() {
const [emails, setEmails] = useState('');
return (
<Card maxWidth={560}>
<VStack gap={4}>
<HStack gap={2} vAlign="center">
<StackItem size="fill">
<Heading level={3} maxLines={1}>
Design review
</Heading>
</StackItem>
<Button label="Request review from the design team" size="sm" />
<IconButton
label="More review options"
icon={<Icon icon="moreHorizontal" />}
variant="ghost"
size="sm"
/>
</HStack>
<TextInput
label="Reviewer emails"
placeholder="name@example.com"
value={emails}
onChange={setEmails}
/>
{/* In a 320px form footer the long primary action truncates.
StackItem size="static" keeps the short Cancel label whole. */}
<HStack gap={2} hAlign="end">
<StackItem size="static">
<Button label="Cancel" variant="secondary" />
</StackItem>
<Button label="Send invitations to all 12 reviewers" variant="primary" />
</HStack>
</VStack>
</Card>
);
}
Theming API
{
"targets": [
{
"className": "astryx-button",
"visualProps": [
"size",
"variant",
"elevation"
]
}
],
"vars": [
{
"name": "--_button-radius",
"description": "Border radius",
"default": "var(--radius-element)",
"private": true
},
{
"name": "--button-focus-offset",
"description": "Focus ring outline offset",
"default": "var(--focus-outline-offset)"
},
{
"name": "--button-icon-only-aspect",
"description": "Aspect ratio for icon-only buttons",
"default": "1 / 1"
}
],
"derived": [
{
"property": "borderRadius",
"vars": [
"--_button-radius"
]
}
]
}
Accessibility
- Text label: Button text must have at least 4.5:1 contrast with the button background in every state. For Hover and Pointer down, measure the final background after the overlay is applied. — 4.5:1
- Essential icon or spinner arc: An icon used instead of text must have at least 3:1 contrast with the button background. The moving spinner arc must also meet 3:1. An icon beside a visible label does not need its own check. — 3:1
- Badge text: Badge text inside a button must have at least 4.5:1 contrast with the Badge background. Check all 14 built-in Badge colors in Rest, Hover, and Pointer down on page and surface backgrounds. This covers 336 pairs per mode. Check custom end content separately. — 4.5:1
- Visible control boundary: The button edge needs 3:1 contrast only when users need it to see the control. A text-only button can rely on its label. — 3:1 if needed
- Keyboard focus indicator: The focus outline needs at least 3:1 contrast with the area around the button. Check every style. Destructive buttons use a red outline. — 3:1
- Disabled appearance: Disabled controls do not need to meet these contrast ratios. — Not required
Anatomy
- Icon: A leading icon that reinforces the label, like a trash icon on a Delete button.
- Label: The visible text describing the action. Also used as the accessible name.
- End content: A trailing badge or icon after the label, like a notification count or dropdown arrow.
- Spinner: Replaces the icon during loading to show the action is in progress.
Examples
- Button — Variants: All four button variants side by side: primary, secondary, ghost, and destructive. A quick visual reference for choosing the right variant.
- Button — Floating: A floating action button raised with
elevation="med". Use elevation for buttons that hover above content, like a FAB. - Button — Sizes: Small, medium, and large buttons side by side. Use small in dense UIs like toolbars, medium for most cases, and large for prominent CTAs.
- Button — Variants: All 4 button variants in default, disabled, and loading states. Use primary for the main action, secondary for most others, ghost for low-emphasis, and destructive for dangerous actions.
- Button — End Slot: Buttons with a trailing badge showing a count or status. Use for notification counts, unread messages, or any button that needs a visual indicator.
- Button — Icon: Buttons with a leading icon that reinforces the label. Use when the icon helps the user identify the action faster, like a plus for "New" or a trash can for "Delete".
Complete example dependencies
Some examples import optional icons. The per-example install command in the examples list identifies exactly what that example needs. For all examples on this page:
npm install @heroicons/react@2.2.0