XDSButton@xds/core · Button

Usage

Button triggers an action when clicked. Use it for form submissions, confirmations, navigation, or any interaction that needs a clear call to action.

Best practices

GuidancePractices
DoReserve primary for the single most important action in the view. Use secondary or ghost for everything else based on emphasis.
DoWrite labels that describe the action — "Save changes", "Delete account", "Send invite" — not vague labels like "OK" or "Click here".
DoShow a loading state for actions that take time, like saving or submitting, so the user knows it is working.
DoAlways provide a label for icon-only buttons so screen readers can announce what the button does. Add a tooltip for sighted users.
Don'tPlace more than one primary button in the same view — this dilutes the visual hierarchy.
Don'tUse the destructive variant without a confirmation step for irreversible actions like deleting data.
Don'tUse 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.

Anatomy

ElementDescription
IconA leading icon that reinforces the label, like a trash icon on a Delete button.
LabelrequiredThe visible text describing the action. Also used as the accessible name.
End contentA trailing badge or icon after the label, like a notification count or dropdown arrow.
SpinnerReplaces the icon during loading to show the action is in progress.

Import

ts
import {XDSButton} from '@xds/core/Button'

Props

PropTypeDescription
labelrequired
stringAccessible label. Rendered as visible text by default; used as aria-label when isIconOnly is true.
variant
'primary' | 'secondary' | 'ghost' | 'destructive' (default: 'secondary')Visual style variant.
size
'sm' | 'md' | 'lg' (default: 'md')Size variant.
type
'button' | 'submit' | 'reset' (default: 'button')HTML button type attribute.
name
stringHTML name attribute for form submission.
value
string | number | readonly string[]HTML value attribute for form submission.
form
stringAssociates the button with a form element by ID.
isLoading
boolean (default: false)Shows a loading spinner and disables interaction. Announces "Loading" via a live region.
isDisabled
boolean (default: false)Disables the button. When a tooltip is present, uses aria-disabled instead of native disabled so the button stays focusable.
icon
ReactNodeIcon element rendered before the label text.
isIconOnly
boolean (default: false)When true, renders as a square icon-only button with label as aria-label. Requires icon.
children
ReactNodeOptional visible content. When provided, rendered instead of label as the visible text.
endContent
ReactElement<XDSIconProps> | ReactElement<XDSBadgeProps>Trailing icon or badge rendered after the label. Ignored when isIconOnly is true. Color is inherited from the button variant.
tooltip
stringTooltip text shown on hover.
onClick
(e: MouseEvent) => voidStandard click handler (passed through from ButtonHTMLAttributes).
clickAction
(e: MouseEvent) => void | Promise<void>Async click handler. Shows loading state while the returned promise is pending.

Examples

Common configurations, variations, and states.
Button — End SlotButtons with a trailing badge showing a count or status. Use for notification counts, unread messages, or any button that needs a visual indicator.
tsx
'use client';
import {XDSButton} from '@xds/core/Button';
import {XDSBadge} from '@xds/core/Badge';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
export default function ButtonWithEndSlot() {
return (
<XDSStack direction="vertical" gap={4}>
<XDSText type="supporting" color="secondary">
Trailing badges for counts or status
</XDSText>
<XDSStack direction="horizontal" gap={3} vAlign="center">
<XDSButton
label="Messages"
variant="primary"
endContent={<XDSBadge variant="info" label={3} />}
/>
<XDSButton
label="Notifications"
variant="secondary"
endContent={<XDSBadge variant="warning" label={12} />}
/>
<XDSButton
label="Updates"
variant="ghost"
endContent={<XDSBadge variant="neutral" label="New" />}
/>
</XDSStack>
</XDSStack>
);
}
Button — IconButtons with a leading icon that reinforces the label. Use when the icon helps the user identify the action faster, like a plus for
tsx
'use client';
import {XDSButton} from '@xds/core/Button';
import {XDSIcon} from '@xds/core/Icon';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
import {
ArrowDownTrayIcon,
PencilSquareIcon,
PlusIcon,
TrashIcon,
} from '@heroicons/react/24/outline';
export default function ButtonWithIcon() {
return (
<XDSStack direction="vertical" gap={4}>
<XDSText type="supporting" color="secondary">
Icons reinforce the action
</XDSText>
<XDSStack direction="horizontal" gap={3} vAlign="center">
<XDSButton
label="New item"
variant="primary"
icon={<XDSIcon icon={PlusIcon} />}
/>
<XDSButton
label="Edit"
variant="secondary"
icon={<XDSIcon icon={PencilSquareIcon} />}
/>
<XDSButton
label="Download"
variant="ghost"
icon={<XDSIcon icon={ArrowDownTrayIcon} />}
/>
<XDSButton
label="Delete"
variant="destructive"
icon={<XDSIcon icon={TrashIcon} />}
/>
</XDSStack>
</XDSStack>
);
}
Button — SizesSmall, medium, and large buttons side by side. Use small in dense UIs like toolbars, medium for most cases, and large for prominent CTAs.
tsx
'use client';
import {XDSButton} from '@xds/core/Button';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
const SIZES = [
{size: 'sm' as const, label: 'Small'},
{size: 'md' as const, label: 'Medium'},
{size: 'lg' as const, label: 'Large'},
];
export default function ButtonSizeVariants() {
return (
<XDSStack direction="vertical" gap={4}>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Primary
</XDSText>
<XDSStack direction="horizontal" gap={3} vAlign="center">
{SIZES.map(({size, label}) => (
<XDSButton key={size} label={label} variant="primary" size={size} />
))}
</XDSStack>
</XDSStack>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Secondary
</XDSText>
<XDSStack direction="horizontal" gap={3} vAlign="center">
{SIZES.map(({size, label}) => (
<XDSButton
key={size}
label={label}
variant="secondary"
size={size}
/>
))}
</XDSStack>
</XDSStack>
</XDSStack>
);
}
Button — VariantsAll 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.
tsx
'use client';
import {XDSButton} from '@xds/core/Button';
import {XDSStack} from '@xds/core/Layout';
import {XDSText} from '@xds/core/Text';
const VARIANTS = [
{variant: 'primary' as const, label: 'Primary'},
{variant: 'secondary' as const, label: 'Secondary'},
{variant: 'ghost' as const, label: 'Ghost'},
{variant: 'destructive' as const, label: 'Destructive'},
];
export default function ButtonVariants() {
return (
<XDSStack direction="vertical" gap={4}>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Default
</XDSText>
<XDSStack direction="horizontal" gap={3} vAlign="center">
{VARIANTS.map(({variant, label}) => (
<XDSButton key={variant} label={label} variant={variant} />
))}
</XDSStack>
</XDSStack>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Disabled
</XDSText>
<XDSStack direction="horizontal" gap={3} vAlign="center">
{VARIANTS.map(({variant, label}) => (
<XDSButton
key={variant}
label={label}
variant={variant}
isDisabled
/>
))}
</XDSStack>
</XDSStack>
<XDSStack direction="vertical" gap={1}>
<XDSText type="supporting" color="secondary">
Loading
</XDSText>
<XDSStack direction="horizontal" gap={3} vAlign="center">
{VARIANTS.map(({variant, label}) => (
<XDSButton
key={variant}
label={label}
variant={variant}
isLoading
/>
))}
</XDSStack>
</XDSStack>
</XDSStack>
);
}

Showcase source

tsx
'use client';
import {XDSButton} from '@xds/core/Button';
import {XDSStack} from '@xds/core/Layout';
export default function ButtonShowcase() {
return (
<XDSStack direction="horizontal" gap={3} vAlign="center">
<XDSButton label="Primary" variant="primary" />
<XDSButton label="Secondary" variant="secondary" />
<XDSButton label="Ghost" variant="ghost" />
<XDSButton label="Destructive" variant="destructive" />
</XDSStack>
);
}