Back to the library

Confounded metric link, slow beat

Variant: Slow beat

The same two-metric card and the same reveal, played on a longer master loop so each phase gets room to be read. Use it on a detail page or an empty-state where the card is the focus and the viewer has time; the stretched hold on the paired traces is what makes the eventual downgrade feel earned. Only the cycle length changes — keyframe percentages, geometry and easing are identical to the single beat.

explanatorydashboardstate-changedataloop

The source

A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Confounded metric link — slow beat</title>
<style>
:root {
  /* Neutrals — fixed, never remixed. */
  --ivory: #faf9f5;
  --paper: #ffffff;
  --oat: #e3dacc;
  --slate: #141413;

  /* Knobs — the remix API. A knob exists iff declared here and used below. */
  --hue: 38.8;
  --lift: 0;
  --chroma: 1;
  --round: 1;
  --speed: 1;

  /* Accents — derived from the knobs with the family's offsets baked in,
     so one hue roll moves them together. Defaults reproduce the original hex
     (#d97757 / #b85c3e / #788c5d / #b04a3f). */
  --clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
  --clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
  --olive: oklch(calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3));
  --rust: oklch(calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3));

  /* Clock — every duration in the document runs off --t. */
  --cycle: 7.2s;
  --t: calc(var(--cycle) / var(--speed));

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);

  --_line-w: 2.25px;
  --_grid: color-mix(in srgb, var(--oat) 70%, var(--paper));
}

* { box-sizing: border-box; }

html, body { margin: 0; }

body {
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: var(--ivory);
  color: var(--slate);
  font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}

.card {
  width: min(400px, 92vw);
  background: var(--paper);
  border: 1px solid var(--oat);
  border-radius: calc(14px * var(--round));
  padding: clamp(12px, 3.4vw, 18px);
  display: grid;
  gap: clamp(8px, 2.2vw, 12px);
}

.head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.title {
  font-size: clamp(12px, 3.2vw, 13.5px);
  font-weight: 600;
  letter-spacing: 0.01em;
}

.verdict {
  position: relative;
  font-size: clamp(10.5px, 2.8vw, 11.5px);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  display: grid;
}

.verdict span {
  grid-area: 1 / 1;
  white-space: nowrap;
}

.v-link { color: var(--clay-d); }
.v-third { color: var(--olive); }

.plot {
  position: relative;
  aspect-ratio: 16 / 8;
  border-radius: calc(10px * var(--round));
  background:
    repeating-linear-gradient(to right, var(--_grid) 0 1px, transparent 1px 25%);
  overflow: hidden;
}

svg { display: block; width: 100%; height: 100%; }

.trace {
  fill: none;
  stroke-width: var(--_line-w);
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 340;
  stroke-dashoffset: 340;
}

.trace-a { stroke: var(--clay); }
.trace-b { stroke: var(--rust); }

.driver {
  fill: none;
  stroke: var(--olive);
  stroke-width: var(--_line-w);
  stroke-linecap: round;
  stroke-dasharray: 340;
  stroke-dashoffset: 340;
  opacity: 0;
}

.tie {
  stroke: var(--clay-d);
  stroke-width: 1.4;
  stroke-dasharray: 3 4;
  opacity: 0;
}

.spoke {
  stroke: var(--olive);
  stroke-width: 1.4;
  stroke-dasharray: 26;
  stroke-dashoffset: 26;
  opacity: 0.9;
}

.legend {
  display: flex;
  gap: clamp(8px, 2.6vw, 14px);
  font-size: clamp(10.5px, 2.7vw, 11.5px);
  color: color-mix(in srgb, var(--slate) 62%, var(--paper));
}

.legend b { font-weight: 600; }

.key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.dot-a { background: var(--clay); }
.dot-b { background: var(--rust); }
.dot-c { background: var(--olive); }

.key-c { opacity: 0; }

/* Identical phase map to the single beat, stretched over a longer clock. */
.trace-a { animation: draw var(--t) var(--ease-in-out) infinite; }
.trace-b { animation: draw var(--t) var(--ease-in-out) infinite; }
.tie     { animation: tie var(--t) var(--ease-out) infinite; }
.driver  { animation: driver var(--t) var(--ease-out) infinite; }
.spoke   { animation: spoke var(--t) var(--ease-out) infinite; }
.spoke-2 { animation-delay: calc(0.06s / var(--speed)); }
.key-c   { animation: keyin var(--t) var(--ease-out) infinite; }
.v-link  { animation: vlink var(--t) ease infinite; }
.v-third { animation: vthird var(--t) ease infinite; }

@keyframes draw {
  0%        { stroke-dashoffset: 340; opacity: 0.9; }
  30%, 52%  { stroke-dashoffset: 0; opacity: 1; }
  74%, 92%  { stroke-dashoffset: 0; opacity: 0.3; }
  100%      { stroke-dashoffset: 0; opacity: 0; }
}

@keyframes tie {
  0%, 24%   { opacity: 0; }
  36%, 52%  { opacity: 0.85; }
  66%, 100% { opacity: 0; }
}

@keyframes driver {
  0%, 50%   { stroke-dashoffset: 340; opacity: 0; }
  56%       { opacity: 1; }
  76%, 92%  { stroke-dashoffset: 0; opacity: 1; }
  100%      { stroke-dashoffset: 0; opacity: 0; }
}

@keyframes spoke {
  0%, 58%   { stroke-dashoffset: 26; opacity: 0; }
  72%, 92%  { stroke-dashoffset: 0; opacity: 0.9; }
  100%      { stroke-dashoffset: 0; opacity: 0; }
}

@keyframes keyin {
  0%, 54%   { opacity: 0; transform: translateY(3px); }
  68%, 92%  { opacity: 1; transform: translateY(0); }
  100%      { opacity: 0; transform: translateY(0); }
}

@keyframes vlink {
  0%, 30%   { opacity: 0; }
  38%, 52%  { opacity: 1; }
  62%, 100% { opacity: 0; }
}

@keyframes vthird {
  0%, 62%   { opacity: 0; }
  72%, 94%  { opacity: 1; }
  100%      { opacity: 0; }
}

@media (max-width: 380px) {
  .card { padding: 11px; gap: 8px; }
  .legend { gap: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  /* Already slow; 1.5x is enough to keep the fades gentle here. */
  .trace, .driver, .spoke {
    stroke-dashoffset: 0;
  }
  .trace-a, .trace-b {
    animation: draw-rm calc(var(--t) * 1.5) ease infinite;
  }
  .driver {
    animation: driver-rm calc(var(--t) * 1.5) ease infinite;
  }
  .spoke {
    animation: spoke-rm calc(var(--t) * 1.5) ease infinite;
    animation-delay: 0s;
  }
  .tie {
    animation: tie calc(var(--t) * 1.5) ease infinite;
  }
  .key-c {
    animation: keyin-rm calc(var(--t) * 1.5) ease infinite;
  }
  .v-link  { animation: vlink calc(var(--t) * 1.5) ease infinite; }
  .v-third { animation: vthird calc(var(--t) * 1.5) ease infinite; }

  @keyframes draw-rm {
    0%, 52%   { opacity: 1; }
    74%, 92%  { opacity: 0.4; }
    100%      { opacity: 1; }
  }
  @keyframes driver-rm {
    0%, 50%   { opacity: 0; }
    76%, 92%  { opacity: 1; }
    100%      { opacity: 0; }
  }
  @keyframes spoke-rm {
    0%, 58%   { opacity: 0; }
    72%, 92%  { opacity: 0.9; }
    100%      { opacity: 0; }
  }
  @keyframes keyin-rm {
    0%, 54%   { opacity: 0; }
    68%, 92%  { opacity: 1; }
    100%      { opacity: 0; }
  }
}
</style>
</head>
<body>
  <div class="card">
    <div class="head">
      <div class="title">Signup rate vs. session length</div>
      <div class="verdict">
        <span class="v-link">Linked</span>
        <span class="v-third">Shared driver</span>
      </div>
    </div>

    <div class="plot">
      <svg viewBox="0 0 320 160" preserveAspectRatio="none" aria-hidden="true">
        <path class="trace trace-a" d="M8 118 C 60 112, 96 96, 132 80 S 216 44, 312 26" />
        <path class="trace trace-b" d="M8 134 C 62 130, 100 118, 138 102 S 220 70, 312 52" />

        <line class="tie" x1="132" y1="80" x2="138" y2="102" />
        <line class="tie" x1="228" y1="49" x2="234" y2="71" />

        <path class="driver" d="M8 152 C 70 150, 110 142, 150 130 S 232 100, 312 84" />
        <line class="spoke spoke-1" x1="150" y1="130" x2="138" y2="102" />
        <line class="spoke spoke-2" x1="150" y1="130" x2="132" y2="80" />
      </svg>
    </div>

    <div class="legend">
      <span class="key"><i class="dot dot-a"></i><b>Signups</b></span>
      <span class="key"><i class="dot dot-b"></i><b>Session</b></span>
      <span class="key key-c"><i class="dot dot-c"></i><b>Campaign spend</b></span>
    </div>
  </div>
</body>
</html>

The variant family

Same idea, one axis moved. Compare them side by side before you commit.