Skip to content
#ask-design-system

Action

Button

Beta

Triggers an action or event, available on web (React) and native (React Native). Supports four variants, four sizes, inverse rendering for branded backgrounds, and disabled and loading states.

Variants

Accent

The single highest-emphasis action on a page — 'Get quotes', 'Book now'. Only available at xl size.

<Button label="Get quotes" />

Primary

Important actions that aren't the page-level CTA — form submissions, confirmations.

<Button label="Continue" variant="primary" />

Secondary

Paired with a primary to offer an alternative — 'Cancel', 'Save draft'. Has a visible border.

<Button label="Save draft" variant="secondary" />

Tertiary

Low-emphasis actions — inline alternatives, toolbar actions. Transparent background.

<Button label="Learn more" variant="tertiary" />

Inverse

Use inverse on dark or branded backgrounds. Toggle to dark theme to see the canvas context.

Accent inverse

<Button label="Get quotes" inverse />

Primary inverse

<Button label="Continue" variant="primary" inverse />

Secondary inverse

<Button label="Save draft" variant="secondary" inverse />

Tertiary inverse

<Button label="Learn more" variant="tertiary" inverse />

Sizes

Small

Dense layouts, tables, or inline with other compact elements.

<Button label="Get quotes" variant="primary" size="sm" />

Medium

The default size for most layouts.

<Button label="Get quotes" variant="primary" />

Large

Prominent form actions and larger touch targets.

<Button label="Get quotes" variant="primary" size="lg" />

Extra large

Page-level CTAs. The only size available for the accent variant.

<Button label="Get quotes" variant="primary" size="xl" />

States

Disabled

Applied when the action is permanently unavailable in the current state.

<Button label="Continue" variant="primary" disabled />

Loading

Replaces the label with a spinner while an async operation is in progress.

<Button label="Submitting" variant="primary" loading />

Full width

Stretches to fill its container. Useful for mobile form submissions or stacked button groups.

<Button label="Get quotes" fullWidth />

When to use

Use a button to trigger an action or transition. Keep labels short and action-oriented — favour verbs like “Save”, “Submit”, or “Get quotes” over vague terms like “Click here”.

Variants

  • Accent: the single most important action on a page — “Get quotes”, “Book now”. One accent button per viewport. Locked to xl size.
  • Primary: important actions that aren’t the page-level CTA — form submissions, confirmations. One primary per distinct area.
  • Secondary: supporting actions paired alongside a primary — “Cancel”, “Save draft”. Has a visible border.
  • Tertiary: low-emphasis actions — inline alternatives, toolbar items, supplementary links-as-buttons.

Do

Use correct hierarchy: one high-emphasis CTA with a lower-emphasis alternative.

Don't

Use multiple high-emphasis buttons at the same level — it removes visual hierarchy.

Sizes

SizeTypical use
xlPage-level CTAs; the only size for accent
lgProminent form actions, larger touch targets
mdDefault for most layouts
smCompact UI — data tables, inline actions

Full width

Use fullWidth for mobile form submissions or stacked button groups where the button should span the container. Avoid it in desktop contexts where a natural content-width button is more appropriate.

When not to use

  • Navigation: use a link (<a> on web, or your app’s navigation primitive on native) instead. Buttons are for actions, not destinations.
  • Destructive actions without confirmation: pair with a confirmation dialog.
  • Multiple accent or primary buttons: if everything is high-emphasis, nothing is.

Props

Prop Type Default Description
label * string - Button text. Also used as the accessible name via aria-label.
variant 'accent' | 'primary' | 'secondary' | 'tertiary' 'accent' Visual style. accent is restricted to size xl at the type level.
size 'sm' | 'md' | 'lg' | 'xl' 'xl' for accent, 'md' for all others Controls padding, font size, and border radius.
inverse boolean false Swaps all colours to their inverse token equivalents. Use on dark or branded backgrounds.
fullWidth boolean false Stretches the button to fill its container width.
disabled boolean false Prevents interaction and applies disabled token colours.
loading boolean false Shows a spinner, disables interaction, and sets aria-busy="true". The label is retained as the accessible name.
onClick () => void - Web — called on click. No-op when disabled or loading.
onPress () => void - Native — called on press. No-op when disabled or loading.
accessibilityLabel string - Native — overrides the default accessible name (which falls back to label).
testID string - Native — passed to the root Pressable and the spinner for test targeting.

Import

Web

import { Button } from '@checkatrade/components-web';

Native

import { Button } from '@checkatrade/components-native';

Basic usage

<Button label="Get quotes" />
<Button label="Continue" variant="primary" />
<Button label="Save draft" variant="secondary" />
<Button label="Cancel" variant="tertiary" />

Inverse on a dark background

<div style={{ background: 'var(--color-background-brand-primary)', padding: '2rem' }}>
  <Button label="Get quotes" inverse />
</div>

Full width

<Button label="Submit" variant="primary" fullWidth />

Loading state

<Button label="Submitting" variant="primary" loading />

Platform status

Platform / AreaStatus
Design (Figma) Beta
Web (React) Beta
Native (React Native) Beta
iOS (Swift) Planned
Android (Kotlin) Planned
Accessibility audit Planned

Accessibility

Web

  • Uses a native <button> element — keyboard accessible by default.
  • aria-label is set from the label prop, so the accessible name is always present even when the label is visually replaced by a spinner during loading.
  • Loading state sets aria-busy="true".
  • Focus ring is provided globally via @checkatrade/tokens/css/basebutton:focus-visible gets a 3px solid outline with 2px offset. The inverse focus ring switches automatically via data-inverse on the underlying element.

React Native

  • Uses Pressable with accessibilityRole="button" — announced as a button by screen readers on iOS (VoiceOver) and Android (TalkBack).
  • accessibilityLabel defaults to label, so the accessible name is always present. Override with the accessibilityLabel prop when the visible label needs context.
  • Loading state sets accessibilityState={{ busy: true }}.
  • Hover state (onHoverIn/onHoverOut) is active on react-native-web for web users; it is a no-op on iOS and Android.

Both platforms

  • Minimum height is 44px, meeting WCAG 2.5.5 (AAA) touch target guidance.
  • accent is locked to xl to ensure the page-level CTA always has sufficient visual weight and touch target size.

Button Changelog

2026-05-08 — fullWidth and hover (PR #26)

Added

  • fullWidth prop — stretches the button to fill its container width.

2026-05-07 — Code review (PR #18)

Changed

  • Focus ring, cursor, and font-weight moved out of component classes into @checkatrade/tokens/css/base. The component no longer hardcodes these — they are provided by a single import at the app level.
  • data-inverse attribute added to the underlying <button> element so base.css can target the inverse focus ring colour via button[data-inverse]:focus-visible.
  • defaultVariants.size removed — it was dead code, always overridden by resolvedSize.
  • Redundant onClick guard removed — <button disabled> natively prevents click events.

Added

  • TypeScript discriminated union: variant="accent" now enforces size="xl" at compile time. Passing a smaller size to an accent button is a type error.
  • Storybook: onPress action wired via argTypes instead of inline in each story arg. Accent story locks size control. AllVariants, AllVariantsInverse, and AllSizes stories added.

Fixed

  • AllVariants story was mixing sizes; all variants now shown at xl for consistent comparison.

2026-04-23 — Initial implementation (PR #2, #15)

Added

  • Button component with accent, primary, secondary, tertiary variants.
  • sm, md, lg, xl sizes with token-backed padding and border radius.
  • inverse, disabled, loading props.
  • resolvedSize logic: accent defaults to xl, others to md.
  • Full token coverage: colours, spacing, typography via Tailwind utilities.
  • Storybook stories: Accent, Primary, Secondary, Tertiary, Disabled, Loading, AllVariants, AllVariantsInverse, AllSizes.
  • Tests: rendering, interaction, disabled state, loading state (12 tests).

Button Changelog (Native)

2026-05-08 — fullWidth and hover (PR #26)

Added

  • fullWidth prop — stretches the button to fill its container via style={{ width: '100%' }}.
  • Hover state via onHoverIn/onHoverOut on Pressable — no-op on iOS/Android, active on react-native-web.

2026-05-07 — Alignment with web Button (PR #18)

Changed

  • Variants renamed to match the web component: accent | primary | secondary | tertiary (previously primary | secondary | outline, where “primary” incorrectly used accent tokens and “outline” used tertiary tokens).
  • Styling rewritten using Unistyles v3 StyleSheet.create(theme => ...) with variants and compoundVariants instead of StyleSheet.flatten with inline conditionals.
  • effectiveVariant pattern removed — disabled state is now its own variant axis (isDisabled) rather than overriding the semantic variant.
  • resolvedSize logic aligned with web: accent defaults to xl, others default to md.
  • Spinner colour resolved directly from theme by props, not extracted from the resolved style object.

Added

  • xl size with token-backed padding and border radius (spacingButtonXl*).
  • inverse prop with compound variants covering all four variants and disabled state.
  • TypeScript discriminated union: variant="accent" enforces size="xl" at compile time.
  • Storybook: onPress action wired via argTypes. AllVariants, AllVariantsInverse, AllSizes stories added.
  • Test: inverse prop rendering (2 tests). “Retains accessible name when loading” test added.

Fixed

  • Font size and line height tokens were silently undefined — keys were typographyLabelFontSizeActionSm (non-existent). Corrected to typographyLabelFontSizeButtonSm/Md/Lg/Xl.
  • Unistyles mock updated to strip variants/compoundVariants from style objects and expose a no-op useVariants so tests run without errors.

2026-04-23 — Initial implementation (PR #2, #3)

Added

  • Button component with primary, secondary, outline variants (using accent, secondary, and tertiary tokens respectively).
  • sm, md, lg sizes.
  • disabled and loading props.
  • accessibilityLabel override prop and testID support.
  • registerTheme helper for Unistyles setup.
  • Tests: rendering, interaction, disabled state, loading state (11 tests).