The unit of intent. Sizes, variants, states and the one warm primary.
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | primary · brand · gradient · ghost · quiet · success · error · info | primary | Visual and semantic role |
| size | l · m · s | m | 44 / 36 / 28px |
| label | string | — | Text content; one line, no wrap |
| icon | ReactNode | — | Optional leading glyph |
| type | button · submit · reset | button | Form semantics |
| href | string | — | Renders an anchor with the button skin |
| disabled | boolean | false | Blocks interaction, drops opacity to 0.38 |
| loading | boolean | false | Shows the spinner, sets aria-busy, locks the button |
| fullWidth | boolean | false | Stretches to the container |
| Event | Payload | Fires when |
|---|---|---|
| onClick | MouseEvent | Pointer press, or Enter / Space when focused |
| onFocus / onBlur | FocusEvent | Keyboard or pointer focus enters / leaves |
| async onClick | Promise | Auto-enters loading; ignored while a call is in flight |
| Enter / Space | — | Native activation; no custom key handler needed |
| Tab | — | Moves focus; disabled buttons are skipped |
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.
Loading while in flight, cancellable via AbortController. A dropped request never overwrites fresher state.
The star fills instantly. One in three requests fails on purpose; watch it roll back and log the truth. Optimism is not denial.
Mash it. While a request is in flight the handler ignores repeats, so a double click never becomes a double charge.
One warm action per view. Everything else steps back so the one light can point forward.
Three warm buttons is zero warm buttons. Competing accents cancel each other out.
Short, sentence-case verb phrases. The label says what happens, in one line.
No shouting, no "click here", no punctuation theatrics. The button is not the copywriter.
Destructive actions wear error red and sit beside an escape hatch.
Never dress a destructive action in the brand accent. Warm means forward, not gone.
Page-level primary sits top right, size M or L, warm if it is the page's one action.
Modal footer: ghost escape on the left, filled confirmation on the right. Size S or M.
Menu items are quiet buttons, size S, full width. Destructive rows speak error red.
| Token | Value | Role |
|---|---|---|
| --btn-height-l | 44px | Large: hero and page-level actions |
| --btn-height-m | 36px | Medium: the default everywhere |
| --btn-height-s | 28px | Small: dense surfaces, table rows, menus |
| --btn-radius | 8px | 6px at size S |
| --btn-gap | 8px | Icon to label |
| --btn-bg-primary | #F4F4F5 | Primary surface, #121212 text |
| --btn-bg-brand | #FF7A00 | The one light. One per view, maximum |
| --btn-bg-gradient | #FFB020→#FF5E00 | Hero moments; counts as the warm action |
| --btn-bg-success | #1FA35C | Positive confirmations · white label |
| --btn-bg-error | #EB4A4F | Destructive actions · white label |
| --btn-bg-info | #3E63DD | Neutral-informational · white label, 5.21:1 |
| --btn-border-ghost | #333336 | Ghost outline on dark surfaces |
| --btn-spinner | 13px · 0.7s | Loading ring in currentColor, width stays stable |
| --btn-focus | rgba(255,122,0,.35) | 3px focus-visible ring |
<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>