Chapter 03 / 12 · Actions & inputs

Buttons & controls. Every tap, on brand.

Buttons are where brand voice meets user intent. One hero per screen, everything else is quieter. Controls follow the same discipline.

3.1 Primary button

The one hero action per screen. Pink fill, pink shadow, ink-on-white is never primary. Use no more than one per viewport.

Primary

.btn.btn-primary

Lift 1px on hover, press back flush on click. Disabled drops opacity without changing colour.

<button class="btn btn-primary">
  Book a demo <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
</button>
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s-2);
  font: 600 14.5px/1 var(--f-display);
  letter-spacing: -0.005em;
  padding: 11px var(--s-5);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease),
              transform var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease);
  user-select: none;
  white-space: nowrap;
}
.btn:focus-visible { outline: 0; box-shadow: var(--sh-focus); }
.btn:disabled { opacity: .55; cursor: not-allowed; pointer-events: none; }

.btn-primary {
  background: var(--accent); color: var(--paper);
  box-shadow: var(--sh-pink);
}
.btn-primary:hover   { transform: translateY(-1px); filter: brightness(1.04); }
.btn-primary:active  { transform: translateY(0); filter: brightness(0.96); }

3.2 Secondary button

Partner to the primary. White card surface with a hair border that darkens to ink on hover. Pair with primary in hero rows.

Secondary

.btn.btn-secondary

<button class="btn btn-secondary">
  Learn more
</button>
.btn-secondary {
  background: var(--bg-paper);
  color: var(--fg);
  border-color: var(--hair);
}
.btn-secondary:hover {
  border-color: var(--fg);
  background: var(--bg-paper);
  transform: translateY(-1px);
}
.btn-secondary:active { transform: translateY(0); }

3.3 Ghost / tertiary button

The quietest button. Use for dismiss, cancel, or any low-stakes action inside a dense panel. No fill until hovered.

Ghost

.btn.btn-ghost

.btn-ghost {
  background: transparent;
  color: var(--fg);
  border-color: transparent;
}
.btn-ghost:hover {
  background: var(--bg-sunk);
  border-color: var(--hair);
}

3.4 Danger button

Destructive actions only. There's only one red in the system — D64545 — and this is where it lives.

Danger + danger-outline

.btn-danger · .btn-danger-outline

Always confirm destructive actions in a modal before executing.

.btn-danger {
  background: var(--error); color: var(--paper);
}
.btn-danger:hover { filter: brightness(1.05); transform: translateY(-1px); }

.btn-danger-outline {
  background: transparent; color: var(--error-text);
  border-color: rgba(214, 69, 69, 0.3);
}
.btn-danger-outline:hover {
  background: var(--error-soft);
  border-color: var(--error);
}

3.5 Sizes

Four sizes, one default. Hero size is for landing-page CTAs only. Small is for dense UIs (tables, toolbars).

Size scale

sm · md · lg · xl

.btn-sm { padding: 7px var(--s-4); font-size: 13px; border-radius: var(--r-sm); }
.btn     { padding: 11px var(--s-5); font-size: 14.5px; }           /* default */
.btn-lg { padding: 13px var(--s-6); font-size: 15.5px; }
.btn-xl {
  padding: 16px var(--s-7);
  font-size: 17px;
  border-radius: var(--r-lg);
  gap: var(--s-3);
}

3.6 Icon button

Square, 36×36, always with an aria-label. Useful in toolbars and compact UIs. Never substitute for a labelled button on marketing surfaces.

Icon-only

.icon-btn

.icon-btn {
  width: 36px; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  color: var(--fg);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease);
}
.icon-btn:hover {
  border-color: var(--fg);
  color: var(--fg);
}
.icon-btn:focus-visible { outline: 0; box-shadow: var(--sh-focus); }

.icon-btn-primary {
  background: var(--accent); color: var(--paper);
  border-color: transparent;
}
.icon-btn-primary:hover { filter: brightness(1.05); color: var(--paper); }

3.8 Button groups & split

Grouped secondary buttons form a horizontal bar for utility strips. Split buttons pair a primary action with a chevron for alternates.

Button group + split

.btn-group · .btn-split

.btn-group {
  display: inline-flex;
}
.btn-group .btn {
  border-radius: 0;
  border-right-width: 0;
}
.btn-group .btn:first-child { border-top-left-radius: var(--r-md); border-bottom-left-radius: var(--r-md); }
.btn-group .btn:last-child  { border-top-right-radius: var(--r-md); border-bottom-right-radius: var(--r-md); border-right-width: 1px; }
.btn-group .btn:hover { z-index: 1; position: relative; }

.btn-split { display: inline-flex; }
.btn-split-main { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.btn-split-icon {
  padding: 11px var(--s-3);
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  border-left: 1px solid rgba(255,255,255,0.25);
}

3.9 Loading state

Replace the trailing icon with a spinner and lock the button. Keep the label — silence feels broken.

Loading

.btn-loading

.btn-loading { pointer-events: none; }
.spinner {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: spin 0.8s linear infinite;
  opacity: .85;
}
.btn-primary .spinner { border-color: rgba(255,255,255,.45); border-top-color: transparent; }
@keyframes spin { to { transform: rotate(360deg); } }

3.10 Toggle (switch)

Two-state settings. Use switch for boolean preferences; use checkbox for selecting items.

Switch

.switch

.switch {
  display: inline-flex; align-items: center; gap: var(--s-3);
  cursor: pointer;
  font: 500 14.5px/1.3 var(--f-body);
  color: var(--fg);
  user-select: none;
}
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch-track {
  flex: 0 0 40px;
  width: 40px; height: 22px;
  background: var(--bg-deep);
  border-radius: var(--r-pill);
  position: relative;
  transition: background var(--dur-2) var(--ease);
}
.switch-track::after {
  content: ""; position: absolute;
  top: 2px; left: 2px;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--paper);
  box-shadow: var(--sh-1);
  transition: transform var(--dur-2) var(--ease);
}
.switch input:checked + .switch-track { background: var(--accent); }
.switch input:checked + .switch-track::after { transform: translateX(18px); }
.switch input:focus-visible + .switch-track { box-shadow: var(--sh-focus); }
.switch input:disabled ~ * { opacity: .5; }
.switch input:disabled { cursor: not-allowed; }

3.11 Checkbox · radio

Pink fill when selected. Checkbox for many-from-many, radio for one-from-many. Never use checkboxes where a switch belongs.

Checkbox & radio

.cb · .rb

.cb, .rb {
  display: flex; align-items: center; gap: var(--s-3);
  font: 400 14.5px/1.3 var(--f-body); color: var(--fg);
  cursor: pointer; padding: 6px 0;
  user-select: none;
}
.cb input, .rb input { position: absolute; opacity: 0; pointer-events: none; }
.cb-box, .rb-circle {
  flex: 0 0 18px;
  width: 18px; height: 18px;
  background: var(--bg-paper);
  border: 1.5px solid var(--hair);
  transition: background var(--dur-2) var(--ease),
              border-color var(--dur-2) var(--ease);
  position: relative;
}
.cb-box { border-radius: var(--r-xs); }
.rb-circle { border-radius: 50%; }

.cb input:checked + .cb-box {
  background: var(--accent); border-color: var(--accent);
}
.cb input:checked + .cb-box::after {
  content: "";
  position: absolute; inset: 0;
  background: none;
  border-right: 2px solid var(--paper);
  border-bottom: 2px solid var(--paper);
  width: 5px; height: 10px;
  margin: auto; top: -2px; left: 0; right: 0; bottom: 0;
  transform: rotate(45deg);
}

.rb input:checked + .rb-circle { border-color: var(--accent); }
.rb input:checked + .rb-circle::after {
  content: "";
  position: absolute; inset: 3px;
  background: var(--accent); border-radius: 50%;
}

.cb input:focus-visible + .cb-box,
.rb input:focus-visible + .rb-circle { box-shadow: var(--sh-focus); }
.cb-disabled { opacity: .5; cursor: not-allowed; }

3.12 Segmented control

Pill-shaped tab strip for switching between sibling views. Use when options are few (≤5) and mutually exclusive.

Segmented

.seg

.seg {
  display: inline-flex;
  /* warm-3 (lightest warm) for the rail — warm-5/bg-sunk reads as
     "dirty/heavy" against white card surfaces. Dark mode flips back. */
  background: var(--warm-3);
  border: 1px solid var(--hair);
  border-radius: var(--r-pill);
  padding: 3px;
  gap: 0;
}
body[data-theme="dark"] .seg { background: var(--bg-sunk); }
.seg-btn {
  appearance: none; border: 0;
  background: transparent;
  color: var(--fg-dim);
  padding: 7px 14px;
  border-radius: var(--r-pill);
  font: 500 13px/1 var(--f-body);
  cursor: pointer;
  transition: color var(--dur-2) var(--ease),
              background var(--dur-2) var(--ease);
}
.seg-btn:hover { color: var(--fg); }
.seg-btn.is-active {
  background: var(--bg-paper);
  color: var(--fg);
  box-shadow: var(--sh-1);
}
.seg-sm .seg-btn { padding: 5px 11px; font-size: 12px; }

3.13 Close & dismiss

A 28×28 pill with just the × glyph. Always paired with aria-label='Close' or 'Dismiss'. Use the soft variant when sitting inside a warning or info surface.

Close button

.close-btn

.close-btn {
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent;
  border: 0;
  color: var(--fg-dim);
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background var(--dur-2) var(--ease),
              color var(--dur-2) var(--ease);
}
.close-btn:hover { background: var(--bg-sunk); color: var(--fg); }

.close-btn-soft { background: var(--accent-soft); color: var(--accent-text); }
.close-btn-soft:hover { background: var(--accent); color: var(--paper); }

3.14 Anatomy of the primary button

Six tokens assemble the hero CTA.

  1. 1
    1. Fill
    background: var(--accent);
  2. 2
    2. Label
    font-family: var(--f-display); font-weight: 600;
  3. 3
    3. Gap
    gap: var(--s-2);
  4. 4
    4. Padding
    padding: 11px var(--s-5);
  5. 5
    5. Radius
    border-radius: var(--r-md);
  6. 6
    6. Elevation
    box-shadow: var(--sh-pink);