Anatomy
Five parts. Padding and gap come from spacing tokens; radius from --radius-lg2 (12px); height from --control-h-*.
padding
★
Place trade
label
icon
radius · 12px
When to use
- Triggering a primary action ("Place trade", "Confirm withdrawal", "Send")
- Triggering a destructive action ("Cancel order", "Delete account") — always with the
--dangervariant - Submitting a form
When NOT to use
- For navigation — use a link (
<a>) styled like a button if it changes the URL. Buttons are for actions, not navigation. - For toggling state — use a switch or checkbox instead.
- As a tab trigger — use the
.fam-tabprimitive inside a.fam-tablist.
Variants
Four variants, each rendered identically across all 3 brands but tinted with that brand's primary color.
Perpetuals
Barriers
UpsideOnly
| Variant | Class | Use case |
|---|---|---|
| Primary | .fam-btn--primary | The most important action on a surface. One per surface, ideally. |
| Secondary | .fam-btn--secondary | Alternative action — same row as primary, different weight. |
| Ghost | .fam-btn--ghost | Tertiary or de-emphasized. Common for "Cancel" next to a destructive primary. |
| Danger | .fam-btn--danger | Destructive primary. "Delete account", "Cancel order". Use sparingly. |
Sizes
5 sizes mapping to --control-h-* tokens.
| Size | Height | Padding | Radius | Use |
|---|---|---|---|---|
--sm | 32px | 4px / 12px | 10px (lg) | Toolbars, table-row actions, compact UI |
--md | 38px | 6px / 12px | 12px (lg2) | Default. Forms, inline actions |
--lg | 42px | 10px / 16px | 12px (lg2) | Card CTAs, modal footer |
--xl | 48px | 14px / 16px | 12px (lg2) | Hero CTA, "Submit Trade", primary form action |
--icon | 38×38 | 0 | 9999px (pill) | Icon-only. Always include aria-label. |
States
Hover/active states are brand-specific via the motion vocabulary. Hover the buttons below to feel the difference.
Perpetuals · −2px lift
Barriers · no transform
UpsideOnly · −3px + 1.045 scale
Code
Pick your framework. HTML works today; React/Angular wrappers ship in v0.5.
<!-- Wrap any subtree in [data-brand] to set brand colors --> <div data-brand="upsideonly"> <button class="fam-btn fam-btn--primary fam-btn--xl"> Submit Trade </button> <button class="fam-btn fam-btn--ghost fam-btn--md"> Cancel </button> <!-- Icon-only: always include aria-label --> <button class="fam-btn fam-btn--primary fam-btn--icon" aria-label="Settings"> <svg.../> </button> </div>
// Import once at the entry of your app's CSS @use "@perpetuals/design-tokens"; // Use the .fam-btn classes directly in markup, OR extend // in your own classes if you need to add brand-specific variants .my-trade-button { @extend .fam-btn; @extend .fam-btn--primary; @extend .fam-btn--xl; }
// If you're on Tailwind, the family tokens are exported as a preset. // Coming in v0.4 — for now, use the CSS classes directly: <button className="fam-btn fam-btn--primary fam-btn--xl"> Submit Trade </button> // OR equivalent inline Tailwind utilities (loses brand-tinting): <button className="h-12 px-4 rounded-xl bg-green-500 text-black font-semibold"> Submit Trade </button>
// Coming in v0.5 — @perpetuals/ui-react package import { Button } from '@perpetuals/ui-react'; function TradePanel() { return ( <div data-brand="upsideonly"> <Button variant="primary" size="xl"> Submit Trade </Button> <Button variant="ghost" size="md"> Cancel </Button> </div> ); }
<!-- Coming in v0.5 — @perpetuals/ui-angular package --> <div data-brand="barriers"> <button famButton variant="primary" size="xl"> Submit </button> </div>
Props / API (v0.5)
Available once the framework wrappers ship in v0.5. Until then, use the CSS classes directly — the modifier classes are 1:1 with these props.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'ghost' | 'danger' | 'primary' | Visual treatment |
size | 'sm' | 'md' | 'lg' | 'xl' | 'icon' | 'md' | Height + padding scale |
disabled | boolean | false | Greys out + cursor:not-allowed |
loading | boolean | false | Shows inline spinner, disables clicks. Coming in v0.5. |
iconLeft | ReactNode | — | Icon before label |
iconRight | ReactNode | — | Icon after label |
onClick | (e) => void | — | Click handler |
type | 'button' | 'submit' | 'reset' | 'button' | Native button type |
Accessibility
Keyboard
- Tab — moves focus to the button (in document order)
- Enter or Space — activates the button (when focused)
- Focus-visible outline uses
--brand-primarywith a 2px outline + 2px offset
Screen reader
- Native
<button>element is announced as "button" - For icon-only buttons,
aria-labelis required (the visible icon doesn't read as text) - For toggle-state buttons, use
aria-pressedinstead of styling alone - For loading state, set
aria-busy="true"while async work runs
Compliance
- Touch target — All sizes md and above (38px+) meet the 44×44 minimum when padding is included. SM (32h) is below — use only in dense desktop UI, never primary mobile actions.
- Color contrast — Primary variants meet 4.5:1 on their backgrounds (verified in /layout audit)
- Reduced motion — Hover transforms are killed when
prefers-reduced-motion: reduce
Do's and Don'ts
Use a verb that matches the action. Be specific.
Use generic verbs that could mean anything.
One primary per surface. The primary is the most likely next step.
Two primaries. The user can't tell which one to pick.
Use danger variant for destructive actions, paired with a ghost cancel.
Use danger variant for non-destructive actions just for emphasis.
Changelog
v0.3.0-rc.6First per-component documentation page (this one). Sets the template for v0.4.
v0.2.0Renamed
UpSideOnly → UpsideOnly.v0.1.0Initial release. 5 sizes × 4 variants × 3 brands. Brand-specific motion personality (warm / minimal / springy).