Button
One button family, four variants, one async arc. Primary carries the single decisive action per screen; secondary and outline support it; ghost recedes. StatefulButton owns every network round-trip — broadcast, swap, restore — so loading and outcome states are never improvised.
Variants
Four variants map to four levels of commitment. A screen gets at most one primary.
import { Button } from "@coldharbor/ui";
<Button variant="primary">Send Bitcoin</Button>
<Button variant="secondary">View details</Button>
<Button variant="ghost">Dismiss</Button>
<Button variant="outline">Copy address</Button>--ch-primary--ch-primary-fg--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-focus
Sizes
sm for dense toolbars, md as the default, lg for ceremony moments, icon for square glyph-only actions.
Square, rounded-lg. Always give it an aria-label.
<Button size="sm">Copy txid</Button>
<Button size="lg">Create wallet</Button>
<Button size="icon" aria-label="Show QR code">…</Button>--ch-primary--ch-primary-fg--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-focus
Interaction states — every variant
Hover, focus and active are pointer/keyboard states, so they are forced here with static classes and transforms to keep every cell visible at once. Hover also lifts scale to 1.02 and active presses to 0.93 via Motion springs at runtime.
Primary
Static replica of the :hover classes + 1.02 scale.
Static replica of the global focus outline.
whileTap press scale 0.93, held statically.
Secondary
Static replica of the :hover classes + 1.02 scale.
Static replica of the global focus outline.
whileTap press scale 0.93, held statically.
Ghost
Static replica of the :hover classes + 1.02 scale.
Static replica of the global focus outline.
whileTap press scale 0.93, held statically.
Outline
Static replica of the :hover classes + 1.02 scale.
Static replica of the global focus outline.
whileTap press scale 0.93, held statically.
<Button variant="primary" disabled>Send Bitcoin</Button>
{/* hover/focus/active are pointer states — never hard-code them in product code */}--ch-primary--ch-primary-fg--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-focus
StatefulButton — the async arc
Every network action renders all four states through the same control: the label cascades out, the icon slot widens in. While loading the button is disabled and aria-busy.
Disabled + aria-busy while in flight.
import { StatefulButton } from "@coldharbor/ui";
<StatefulButton
state={broadcastState} // "idle" | "loading" | "success" | "error"
loadingText="Broadcasting…"
successText="Broadcast"
errorText="Try again"
>
Send Bitcoin
</StatefulButton>--ch-primary--ch-primary-fg--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-focus
MagneticButton, ButtonLink & ripple
Magnetic pull and ripple are live pointer behaviors — move the cursor over them. Both collapse under reduced motion.
Cursor-attracted; strength 0.25 default.
Anchor with button skin — external explorer links.
ripple spawns from the press point; press to see it.
import { ButtonLink, MagneticButton } from "@coldharbor/ui";
<MagneticButton strength={0.25}>Restore wallet</MagneticButton>
<ButtonLink href="/explorer/tx/…">View on explorer</ButtonLink>
<Button ripple>Verify backup</Button>--ch-primary--ch-primary-fg--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-focus
Themes
The hero pairing — one primary action with its secondary escape — under all three layers.
--ch-primary--ch-primary-fg--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-focus