Back to the library
Confounded metric link, offset traces
Variant: Offset traces
Same card, but the two correlated traces no longer draw in unison — the second is delayed by a 90ms sub-clock, and the tie marks and driver spokes cascade behind it at 60ms steps. Reach for this when the card sits alone and you want the eye led along the pair one line at a time before the third variable arrives. The offsets are short enough that the traces still read as one gesture rather than two separate events.
explanatorydashboardstaggerdataloop
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 — offset traces</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: 5.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));
--_step: calc(0.09s / var(--speed)); /* trace offset */
--_step-s: calc(0.06s / var(--speed)); /* tie / spoke cascade */
}
* { 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; }
/* Same phase map; the pair is pulled apart by a 90ms sub-clock so the eye
tracks one line then the other before the driver answers both. */
.trace-a { animation: draw var(--t) var(--ease-in-out) infinite; }
.trace-b {
animation: draw var(--t) var(--ease-in-out) infinite;
animation-delay: var(--_step);
}
.tie { animation: tie var(--t) var(--ease-out) infinite; }
.tie-2 { animation-delay: var(--_step-s); }
.driver {
animation: driver var(--t) var(--ease-out) infinite;
animation-delay: var(--_step);
}
.spoke { animation: spoke var(--t) var(--ease-out) infinite; }
.spoke-1 { animation-delay: var(--_step); }
.spoke-2 { animation-delay: calc(var(--_step) + var(--_step-s)); }
.key-c {
animation: keyin var(--t) var(--ease-out) infinite;
animation-delay: calc(var(--_step) + var(--_step-s));
}
.v-link { animation: vlink var(--t) ease infinite; }
.v-third {
animation: vthird var(--t) ease infinite;
animation-delay: var(--_step);
}
@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) {
/* The cascade survives as a fade cascade: offsets kept, travel dropped,
cycle stretched to ~1.8x. */
.trace, .driver, .spoke {
stroke-dashoffset: 0;
}
.trace-a, .trace-b {
animation: draw-rm calc(var(--t) * 1.8) ease infinite;
}
.driver {
animation: driver-rm calc(var(--t) * 1.8) ease infinite;
}
.spoke {
animation: spoke-rm calc(var(--t) * 1.8) ease infinite;
}
.tie {
animation: tie calc(var(--t) * 1.8) ease infinite;
}
.key-c {
animation: keyin-rm calc(var(--t) * 1.8) ease infinite;
}
.v-link { animation: vlink calc(var(--t) * 1.8) ease infinite; }
.v-third { animation: vthird calc(var(--t) * 1.8) 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 tie-1" x1="132" y1="80" x2="138" y2="102" />
<line class="tie tie-2" 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.