The thing you tap to do something. Three variants — primary, secondary, link — and no disabled state.
Light
Primary · full width by default
Record promotionPrimary · hugging, trailing glyph
Check inSecondary
Add a noteLink
See all classesDestructive · the secondary style, an ink outline
Delete eventDark
Primary · full width by default
Record promotionPrimary · hugging, trailing glyph
Check inSecondary
Add a noteLink
See all classesDestructive · the secondary style, an ink outline
Delete eventPart of the Konjo design language. The rules for when each variant is right live in buttons; this page is the component. Source:
Button.tsx.
When to use it
- Primary for the one action the screen exists for: "Record promotion", "Check in". At most one per screen, and zero is normal on an index.
- Secondary for every other action that deserves a shape: "Add a note", "Try again" under a failed section.
- Link for a quiet way out or a way further: "See all classes", "Not now".
- Destructive is not a variant. It is a secondary with a plain verb ("Delete event"), behind the serious-action confirmation.
Not for navigation inside a list — that is a Row. Not for picking a value — that is a chip, a segmented control or a select trigger.
Anatomy
| Part | Primary | Secondary | Link |
|---|---|---|---|
| Height floor | controlHeight.primary 48 |
controlHeight.control 44 |
touchTarget 44, plus hitSlop 12 left and right |
| Padding | spacing.s12 × spacing.s20 |
spacing.s12 × spacing.s16 |
spacing.s12 vertical |
| Shape | radius.pill |
radius.pill |
— |
| Fill | brand.red |
none | none |
| Edge | none | 1px border.control |
none |
| Label | typography.rowTitle 16/600, brand.onRed |
typography.label 13/700, text.primary |
typography.label 13/700, accent.red |
| Glyph gap | spacing.s6 |
spacing.s6 |
spacing.s6 |
Leading and trailing glyphs are iconSize.inline 16 in the label's colour. A primary is full
width by default; secondary and link hug their label.
States
Light
Default
SavePressed · opacity 0.85
SaveFocus · 2px ring
SaveLoading
Submitting…Secondary · focus
Add a noteSecondary · loading
Submitting…Dark
Default
SavePressed · opacity 0.85
SaveFocus · 2px ring
SaveLoading
Submitting…Secondary · focus
Add a noteSecondary · loading
Submitting…- Default as drawn above.
- Pressed: the whole button drops to
opacity.pressed0.85. Nothing else moves. - Focus: a 2px
border.focusring outside the shape, on the page behind it. A hero CTA's ring flips toborder.focusOnHero— see Hero. - Loading: the fill stays, the label swaps to "Submitting…" (or your
loadingLabel), a 16pt spinner takes the leading slot, taps are ignored andaccessibilityState={{ busy: true }}is set. The button stays exactly as loud. - Disabled: there is none, on purpose. There is no legal disabled text colour, so a greyed label is either unreadable or lying about being tappable. Validate on tap and jump to the first problem instead.
- Error: a failed submit relabels the primary "Try again" and puts a bordered block above it. The form stays.
On web the ring is the browser's. Button does not draw its own focus ring; on RN-web it relies on
the browser's default focus outline. Input, SearchField and SelectTrigger draw the 2px
border.focus themselves; the specimen shows the ring this component owes.
Content
- Sentence case, a verb first: "Record promotion", not "Promotion" or "OK".
- Never type capitals — VoiceOver spells them. The only uppercase button is the CTA inside a
Hero, and that is applied with
textTransform. - Name the thing: "Delete event", not "Delete". The label is the last thing read before a permanent change.
- Long labels wrap; the button grows. Never truncate.
Accessibility
- Role
button. The accessible name defaults to the visible label; passaccessibilityLabelonly when the label alone is a fragment. - Targets are 44pt or more: primary is 48, secondary 44, and a link gets
hitSlopto make up the width its text does not have. brand.onRedonbrand.redis 5.00:1 in both themes.accent.redonsurface.pageis 5.25:1 in light; that is why a link isaccent.redand neverbrand.red.- Loading announces as busy rather than disabled, so a screen reader hears that the tap landed.
Do and don't
Do
An ink outline and a plain verb, behind a confirmation that names what goes.
Don’t
A red fill. Red means “tap here”, so a red Delete reads as the thing to press.
Choose who conferred it.
Do
Submit stays live. The tap validates and lands on the first problem.
Don’t
A greyed submit. It is illegible or lying, and it never says what is missing.
Do
A 48pt primary carries rowTitle 16/600, in sentence case.
Don’t
A 48pt button carrying 13pt capitals: target size and label size decided apart.
Code
import { Button } from '../../components/ui';
<Button
label="Record promotion"
onPress={submit}
loading={saving}
testID="promotion-submit"
/>
<Button variant="secondary" label="Delete event" onPress={armDelete} testID="event-delete" />
<Button variant="link" label="See all classes" onPress={openClasses} testID="train-all-classes" />
tone="red" is deprecated and changes nothing: red is the default action colour.