Button

The unit of intent. Sizes, variants, states and the one warm primary.

v1.0.0·Updated Jul 9, 2026 · 12:00
Open the Figma file ↗
Live Demo — Fully Interactive
Sizes — Primary
L · 44px
M · 36px
S · 28px
Variants — M size
Primary#F4F4F5
Brand#FF7A00 · one per view
Gradient#FFB020 → #FF5E00 · hero moments only
Quiettext only
Semantic — status actions
Success#1FA35C · white label
Error#EB4A4F · destructive, confirm first
Info#3E63DD · white, 5.21:1
Configuration
Variant
Size
State
Icon
Render as
Target
Label
<Button variant="primary" size="m">Label</Button>
Behavior — The Engineering Contract
Props
PropTypeDefaultDescription
variantprimary · brand · gradient · ghost · quiet · success · error · infoprimaryVisual and semantic role
sizel · m · sm44 / 36 / 28px
labelstringText content; one line, no wrap
iconReactNodeOptional leading glyph
typebutton · submit · resetbuttonForm semantics
hrefstringRenders an anchor with the button skin
disabledbooleanfalseBlocks interaction, drops opacity to 0.38
loadingbooleanfalseShows the spinner, sets aria-busy, locks the button
fullWidthbooleanfalseStretches to the container
Events & keyboard
EventPayloadFires when
onClickMouseEventPointer press, or Enter / Space when focused
onFocus / onBlurFocusEventKeyboard or pointer focus enters / leaves
async onClickPromiseAuto-enters loading; ignored while a call is in flight
Enter / SpaceNative activation; no custom key handler needed
TabMoves focus; disabled buttons are skipped
Live behavior — every demo is real
Click & KeyboardonClick
fired 0×
via

A real button responds to pointer and to Enter/Space with no extra code. Keyboard presses report detail 0; that is how we tell them apart.

Async GET · fetchGET /clients
ready

Loading while in flight, cancellable via AbortController. A dropped request never overwrites fresher state.

Optimistic POSTPOST /favorite
state off
press to fire a request

The star fills instantly. One in three requests fails on purpose; watch it roll back and log the truth. Optimism is not denial.

Double-Submit GuardPOST /order
placed 0
blocked 0

Mash it. While a request is in flight the handler ignores repeats, so a double click never becomes a double charge.

Anatomy
1ContainerFixed height per size, radius 8, holds everything with 0 vertical padding.
2Leading iconOptional, 14px at L, 8px gap to the label. Never two icons.
3LabelInter 500, sentence case, one line, no wrapping ever.
4Focus ring3px warm ring at 35 percent, on focus-visible only.
Do's and Don'ts
✓ Do

One warm action per view. Everything else steps back so the one light can point forward.

✕ Don't

Three warm buttons is zero warm buttons. Competing accents cancel each other out.

✓ Do

Short, sentence-case verb phrases. The label says what happens, in one line.

✕ Don't

No shouting, no "click here", no punctuation theatrics. The button is not the copywriter.

✓ Do

Destructive actions wear error red and sit beside an escape hatch.

✕ Don't

Never dress a destructive action in the brand accent. Warm means forward, not gone.

In context: Examples
A · In a page

Page-level primary sits top right, size M or L, warm if it is the page's one action.

B · In a modal

Modal footer: ghost escape on the left, filled confirmation on the right. Size S or M.

C · In a menu

Menu items are quiet buttons, size S, full width. Destructive rows speak error red.

Measurements & Tokens
TokenValueRole
--btn-height-l44pxLarge: hero and page-level actions
--btn-height-m36pxMedium: the default everywhere
--btn-height-s28pxSmall: dense surfaces, table rows, menus
--btn-radius8px6px at size S
--btn-gap8pxIcon to label
--btn-bg-primary#F4F4F5Primary surface, #121212 text
--btn-bg-brand#FF7A00The one light. One per view, maximum
--btn-bg-gradient#FFB020→#FF5E00Hero moments; counts as the warm action
--btn-bg-success#1FA35CPositive confirmations · white label
--btn-bg-error#EB4A4FDestructive actions · white label
--btn-bg-info#3E63DDNeutral-informational · white label, 5.21:1
--btn-border-ghost#333336Ghost outline on dark surfaces
--btn-spinner13px · 0.7sLoading ring in currentColor, width stays stable
--btn-focusrgba(255,122,0,.35)3px focus-visible ring
Code
<button class="o-btn o-btn--m o-btn--primary">Label</button>

<button class="o-btn o-btn--m o-btn--brand">The one light</button>

<button class="o-btn o-btn--m o-btn--primary is-loading" aria-busy="true">
  <span class="o-btn__spinner"></span> Saving
</button>