04 — Components

Buttons & CTAs

10 variants · 8 sizes · 5 interaction states. All documented with exact token values. Built on shadcn/ui CVA with CPSL design tokens.

All Variants — Live Interactive

Real Button components — hover, click, and Tab through them to see all states in action.

Interaction States — Documented

Variant

Default

Resting state. No interaction.

:hover

bg opacity → 88%. Cursor: pointer.

:focus-visible

3px ring · ring-ring/50 · #697279 at 50%.

:active

bg opacity → 82%. scale(0.98).

:disabled

opacity-50 · pointer-events-none.

Primary (default)

Default
:hover

opacity(bg) → 88%

Focused

3px · #697279 · 50% alpha

:active

opacity(bg) → 82% + scale(0.98)

:disabled

opacity-50 · no events

cpsl-crimson

Default
:hover

opacity(bg) → 88%

Focused

3px · #697279 · 50% alpha

:active

opacity(bg) → 82% + scale(0.98)

:disabled

opacity-50 · no events

Secondary

Default
:hover

opacity(bg) → 88%

Focused

3px · #697279 · 50% alpha

:active

opacity(bg) → 82% + scale(0.98)

:disabled

opacity-50 · no events

Ghost

Default
:hover

opacity(bg) → 88%

Focused

3px · #697279 · 50% alpha

:active

opacity(bg) → 82% + scale(0.98)

:disabled

opacity-50 · no events

Destructive

Default
:hover

opacity(bg) → 88%

Focused

3px · #697279 · 50% alpha

:active

opacity(bg) → 82% + scale(0.98)

:disabled

opacity-50 · no events

Focus Ring — Keyboard Navigation

Tab to focus these buttons →

Press Tab to cycle through buttons and see the live focus ring.

Focus ring anatomy

Property
Value
Token
box-shadow style
ring (not outline)
focus-visible:ring-[3px]
ring color
#697279 at 50% alpha
ring-ring/50
ring width
3px
ring-[3px]
visibility
keyboard only
focus-visible (not focus)
never suppressed
outline: none is not used
WCAG 2.4.7

State CSS Reference

css
/* ── State tokens for Primary button ──────────────────────── */

/* Default */
.btn-primary {
  background-color: var(--primary);          /* #697279 */
  color: var(--primary-foreground);          /* #ffffff */
}

/* Hover */
.btn-primary:hover {
  background-color: color-mix(in srgb, var(--primary) 88%, transparent);
}

/* Focus (keyboard only) */
.btn-primary:focus-visible {
  box-shadow: 0 0 0 3px oklch(from var(--ring) l c h / 50%);
  /* ring = #697279 → rgba(0,71,255,0.5) */
}

/* Active / Pressed */
.btn-primary:active {
  background-color: color-mix(in srgb, var(--primary) 82%, transparent);
  transform: scale(0.98);
}

/* Disabled */
.btn-primary:disabled {
  opacity: 0.5;
  pointer-events: none;
  cursor: not-allowed;
}

Sizes

xs · h-6
sm · h-8
default · h-9
lg · h-11
xl · h-13
pill · rounded-full
pill-sm
pill-lg

Icon Buttons

Common Patterns

Primary + Secondary CTA

Match Actions

Destructive Confirm

Implementation

tsx
import { Button } from "@/components/ui/button"

// Core CPSL variants
<Button variant="default">Primary</Button>
<Button variant="cpsl-crimson">Crimson CTA</Button>
<Button variant="cpsl-navy">Navy</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="cpsl-success">Success</Button>
<Button variant="cpsl-live">Live Match</Button>

// Sizes
<Button size="xs" />   // h-6
<Button size="sm" />   // h-8
<Button size="default" />  // h-9
<Button size="lg" />   // h-11
<Button size="xl" />   // h-13
<Button size="pill" />         // h-9 · rounded-full
<Button size="pill-lg" />      // h-11 · rounded-full

// Disabled
<Button disabled>Disabled</Button>

Accessibility Checklist

44×44px min touch target
All sizes at sm and above meet WCAG 2.5.5. icon-xs is docs-only.
Focus ring always visible
3px ring at 50% alpha on :focus-visible. Never suppressed with outline:none.
Keyboard only focus
focus-visible means mouse clicks don't trigger the ring — only Tab/keyboard.
Disabled ≠ aria-hidden
Disabled buttons stay in tab order with pointer-events-none. Use aria-disabled for cleaner UX.
Icon buttons need aria-label
All icon-only buttons require aria-label describing the action.
asChild for links
Use <Button asChild><Link> to get button styles on Next.js Link without nesting <button> inside <a>.