Components / Button
Button review
The primary action control — a capsule/rounded control carrying a text label and optional icon. Drives registration, navigation, and form-submit actions across the website and app.
Source: specs/button/spec.json · specs/button/notes.md
Live preview
Rendered from the tier-2 component tokens + foundation tokens per this spec — a spec-faithful rendering, not the production component (those live in each product repo). Variants and states come from the spec below.
primary
secondary
Anatomy
root- The button box: background, radius, padding, focus ring.
label- The text label — weight semibold, size driven by the size axis.
icon- Optional leading/trailing icon (e.g. the '→' on 'Continue'); inherits the label colour.
Token bindings
Each binding's value is resolved live from the built
@beestera/tokens output — tier-2 component tokens
(--button-*) collapse through the foundation tier.
| Part | Property | Token | Resolved | Applies to |
|---|---|---|---|---|
root | border-radius | --button-radius | 0.875rem --button-radius → --radius-lg | — |
root | background | --button-bg | #ffd700 --button-bg → --color-brand | variant:primary |
root | background | --button-bg-hover | #ffbb00 --button-bg-hover → --color-brand-strong | variant:primary,state:hover |
label | color | --button-fg | #181818 --button-fg → --color-ink | variant:primary |
root | background | --button-secondary-bg | #ffffff --button-secondary-bg → --color-surface | variant:secondary |
root | background | --button-secondary-bg-hover | #fafafa --button-secondary-bg-hover → --color-surface-alt | variant:secondary,state:hover |
root | border-color | --button-secondary-border | #e4e4e7 --button-secondary-border → --color-border | variant:secondary |
label | color | --button-fg | #181818 --button-fg → --color-ink | variant:secondary |
root | outline-color | --button-focus-ring | #181818 --button-focus-ring → --color-ink | state:focus |
root | box-shadow | --button-shadow | 0px 1px 2px 0px #0000000d, inset 0px -2px 0px 0px #0000000d, inset 0px 0px 0px 1px #0000002e --button-shadow → --shadow-xs-skeuomorphic | variant:primary |
root | background | --button-disabled-bg | #d4d4d8 --button-disabled-bg → --bg-quaternary → --color-neutral-300 | state:disabled |
label | color | --button-disabled-fg | #71717a --button-disabled-fg → --text-quaternary → --color-neutral-500 | state:disabled |
label | font-size | --text-small | 0.75rem | size:sm |
label | font-size | --text-body | 1rem | size:md |
Variants
-
variantprimarysecondary - Visual emphasis. primary = brand-yellow fill with ink label (the main CTA); secondary = surface fill with a hairline border (lower-emphasis actions).
-
sizesmmd - Control size — drives padding and label scale, both on the spacing grid. sm for dense contexts, md as the default.
State matrix
default- Resting appearance for the active variant.
hover- Background deepens (primary -> brand-strong; secondary -> surface-alt).
active- Pressed — matches the hover background.
focus- A 2px ink outline offset 2px from the box, visible on both brand and surface backgrounds.
disabled- Non-interactive: a bg-quaternary fill with a text-quaternary label, the brand gradient and the skeuomorphic shadow both dropped, and no pointer or keyboard activation. The Figma Primary/Disabled treatment drew the active brand gradient until the client corrected it on 2026-08-06 — which is why a sold-out camp card looked registerable. (Not yet in @repo/ui — see notes.)
Accessibility
- Role
- button (native <button>) or link (<a>) when it navigates
- Contrast
- Label vs background >= 4.5:1 (WCAG AA text). Ink on brand-yellow passes; ink on surface passes.
- Focus order
- Focusable in DOM order; focus ring MUST be visible (focus-visible), never suppressed.
- Target size
- Interactive height >= 44px (--spacing-touch-min) at md; sm reserved for dense, non-primary contexts.
- Never yellow text on a light background (brand rule) — primary uses ink text on the yellow fill.
- Icon is decorative; the label carries the accessible name.
Usage
Do
- Use exactly one primary button per view for the main action.
- Keep labels short and imperative ('Register', 'Find a Camp', 'Continue').
- Render as <a> when the action navigates, <button> when it mutates.
Don't
- Don't stack multiple primary buttons competing for attention.
- Don't hardcode the radius/colour — bind to the --button-* tokens.
- Don't give a Primary button a decorative rgba border — the unbound rgba(255,255,255,0.12) stroke was removed from all 50 Figma Primary variants on 2026-08-06. The lit top edge comes from --button-shadow's insets.
- Don't remove the focus ring.
Implementations
-
swarm-os -
packages/ui/src/Button.ts—buildButtonViewShips variant: primary|secondary, size: sm|md. Currently composes Tailwind utilities (bg-brand, rounded-lg, ...) onto the foundation tier; can migrate to var(--button-*) on its own cadence.
Figma source: App Buttons