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
Inside an inverse <Surface> this happens automatically — Surface is the source of truth. Pass inverse explicitly only for a dark or branded background that isn’t a Surface. Toggle to dark theme to see the canvas context.
Accent inverse
<Surface tone="inverse" className="rounded-lg p-6">
<Button label="Get quotes" inverse />
</Surface>Primary inverse
<Surface tone="inverse" className="rounded-lg p-6">
<Button label="Continue" variant="primary" inverse />
</Surface>Secondary inverse
<Surface tone="inverse" className="rounded-lg p-6">
<Button label="Save draft" variant="secondary" inverse />
</Surface>Tertiary inverse
<Surface tone="inverse" className="rounded-lg p-6">
<Button label="Learn more" variant="tertiary" inverse />
</Surface>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
xlsize. - 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
| Size | Typical use |
|---|---|
xl | Page-level CTAs; the only size for accent |
lg | Prominent form actions, larger touch targets |
md | Default for most layouts |
sm | Compact 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 | surrounding Surface tone | Swaps all colours to their inverse token equivalents. Defaults to the surrounding Surface tone — inverse inside `<Surface tone="inverse">`. Pass explicitly to force it on/off, e.g. on a dark background that is not a Surface. |
| 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-surface-brand-primary-default)', 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 / Area | Status |
|---|---|
| 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-labelis set from thelabelprop, 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/base—button:focus-visiblegets a 3px solid outline with 2px offset. The inverse focus ring switches automatically viadata-inverseon the underlying element.
React Native
- Uses
PressablewithaccessibilityRole="button"— announced as a button by screen readers on iOS (VoiceOver) and Android (TalkBack). accessibilityLabeldefaults tolabel, so the accessible name is always present. Override with theaccessibilityLabelprop 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. accentis locked toxlto 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
fullWidthprop — 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-inverseattribute added to the underlying<button>element sobase.csscan target the inverse focus ring colour viabutton[data-inverse]:focus-visible.defaultVariants.sizeremoved — it was dead code, always overridden byresolvedSize.- Redundant
onClickguard removed —<button disabled>natively prevents click events.
Added
- TypeScript discriminated union:
variant="accent"now enforcessize="xl"at compile time. Passing a smaller size to an accent button is a type error. - Storybook:
onPressaction wired viaargTypesinstead of inline in each story arg.Accentstory locks size control.AllVariants,AllVariantsInverse, andAllSizesstories added.
Fixed
AllVariantsstory was mixing sizes; all variants now shown atxlfor consistent comparison.
2026-04-23 — Initial implementation (PR #2, #15)
Added
Buttoncomponent withaccent,primary,secondary,tertiaryvariants.sm,md,lg,xlsizes with token-backed padding and border radius.inverse,disabled,loadingprops.resolvedSizelogic:accentdefaults toxl, others tomd.- 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
fullWidthprop — stretches the button to fill its container viastyle={{ width: '100%' }}.- Hover state via
onHoverIn/onHoverOutonPressable— 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(previouslyprimary | secondary | outline, where “primary” incorrectly used accent tokens and “outline” used tertiary tokens). - Styling rewritten using Unistyles v3
StyleSheet.create(theme => ...)withvariantsandcompoundVariantsinstead ofStyleSheet.flattenwith inline conditionals. effectiveVariantpattern removed — disabled state is now its own variant axis (isDisabled) rather than overriding the semantic variant.resolvedSizelogic aligned with web:accentdefaults toxl, others default tomd.- Spinner colour resolved directly from theme by props, not extracted from the resolved style object.
Added
xlsize with token-backed padding and border radius (spacingButtonXl*).inverseprop with compound variants covering all four variants and disabled state.- TypeScript discriminated union:
variant="accent"enforcessize="xl"at compile time. - Storybook:
onPressaction wired viaargTypes.AllVariants,AllVariantsInverse,AllSizesstories 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 weretypographyLabelFontSizeActionSm(non-existent). Corrected totypographyLabelFontSizeButtonSm/Md/Lg/Xl. - Unistyles mock updated to strip
variants/compoundVariantsfrom style objects and expose a no-opuseVariantsso tests run without errors.
2026-04-23 — Initial implementation (PR #2, #3)
Added
Buttoncomponent withprimary,secondary,outlinevariants (using accent, secondary, and tertiary tokens respectively).sm,md,lgsizes.disabledandloadingprops.accessibilityLabeloverride prop andtestIDsupport.registerThemehelper for Unistyles setup.- Tests: rendering, interaction, disabled state, loading state (11 tests).