Buttons
Always <Button> — there's no hand-styled-<button> escape hatch anymore. size, icon, and variant cover the dimensions and colors a button can need. Never override its visuals via class/style.
Variants
From zeyo-ui/form. Layout-only classes (mt-4, w-full) are fine — never padding/background/color/border.
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="ghost-destructive">Ghost destructive</Button>
<Button variant="link">Link</Button>Decorative variant
An animated gradient CTA for a handful of high-stakes spots (pricing, waitlist signup) — not general-purpose, but shared like every other variant. Originated as marketing's own brand fork; promoted into zeyo-ui/template.css so any app can reach for it without re-forking the CSS.
<Button variant="shiny">Get started</Button>Sizes
Omit size for the default 35px height. size='small' is the one shorter footprint (28px) — for toolbars, inline row actions, dense panels.
<Button>Default (35px)</Button>
<Button size="sm">Small (28px)</Button>Icon
Square, zero-padding footprint for a single icon child. Combines with size (icon size='small' = 28×28, icon alone = 35×35). Always pair with aria-label — there's no visible text.
<Button icon variant="ghost" aria-label="Edit"><Pencil size={14} /></Button>
<Button icon size="sm" variant="ghost" aria-label="Delete"><Trash2 size={12} /></Button>States
loading (or a wired-up form prop) swaps the label for a spinner automatically — no manual isPending wiring.
<Button disabled>Disabled</Button>
<Button loading>Loading</Button>
<Button loading loadingMessage="Saving…">Save</Button>As a link
Passing href renders an <a class='btn ...'> instead of a <button> — same visual, correct semantics for navigation. Route strings always go through resolve().
<Button href={resolve('/dashboard')}>Renders as <a></Button>