web design
COMPONENT N / ALPHA CURVE · SCROLL-SCRUBBED PATH DRAW + COUNTER

The one allowed data moment.

Scroll to draw the curve; the counter climbs with it. Illustrative only.

Cumulative alpha · illustrative

Portfolio value: $100,000

Acct: hallowed finance trader

GUIDE / ALPHA CURVE

Overview

The single permitted data moment on the page: a bespoke gold-on-dark alpha curve that draws itself in on scroll, with a HUD-style portfolio counter that counts up in sync. The path draw (stroke-dashoffset) and the counter tween ride the same ScrollTrigger scrub, so the number tracks the line's travel exactly.

When to use

Once per page, as a single credibility beat. Never more than one. If you already have a chart, sparkline, or count-up anywhere else, this is not the place for a second.

Spec

Path
strokeDasharray = totalLength; scrub top 88% → bottom 62%
Counter
100,000 → 1,000,000, formatUsd on update
Reduced
offset 0, counter at final value, no ScrollTrigger
A11y
role img, labelled illustrative

Anti-patterns

No real data behind it. No second chart on the same page. No count-up without the draw, no draw without the count-up. No replay button; scroll-scrubbed scenes re-trigger on scroll.

<!-- Requires GSAP: https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js -->

<style>
:root{
  --bg:#0b0a08;--surface:#14110b;--ivory:#efe8d6;--muted:#9c9078;--faint:#5a5446;
  --line:#2a2519;--line-faint:rgba(239,232,214,.08);--gold:#c9a24b;--gold-soft:#e7cd8c;
  --font-display:'Instrument Serif',ui-serif,Georgia,'Times New Roman',serif;
  --font-title:'Libre Caslon Condensed',ui-serif,Georgia,'Times New Roman',serif;
  --font-body:'Inter',system-ui,-apple-system,sans-serif;
  --font-mono:'JetBrains Mono',ui-monospace,monospace;
  --ease:cubic-bezier(.22,1,.36,1);--dur-fast:200ms;--dur-base:400ms;
}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--ivory);font-family:var(--font-body);min-height:100vh;padding:64px 48px}
.intro{max-width:960px;margin:0 auto 48px}
.intro h1{font-family:var(--font-display);font-size:2.5rem;font-weight:400;line-height:1.1;color:var(--ivory)}
.intro p{margin-top:16px;color:var(--muted);font-size:.9375rem;line-height:1.65;max-width:60ch}
.spacer{min-height:28vh}
.stage{max-width:960px;margin:0 auto}
.alpha-fig{
  border-radius:1.5rem;
  border:1px solid var(--line);
  background:rgba(20,17,11,.4);
  padding:2.5rem;
  overflow:hidden;
}
.alpha-head{display:flex;justify-content:space-between;align-items:flex-start;gap:1rem;flex-wrap:wrap;margin-bottom:1.25rem}
.alpha-head figcaption,
.alpha-head .pv{
  font-family:var(--font-mono);font-size:.7rem;text-transform:uppercase;
  letter-spacing:.3em;color:var(--faint);
}
.alpha-head .pv{white-space:nowrap}
.alpha-head .pv b{font-weight:500;font-variant-numeric:tabular-nums;letter-spacing:normal;color:var(--gold-soft)}
.alpha-fig svg{width:100%;height:auto;display:block}
.alpha-foot{margin-top:1rem;font-family:var(--font-mono);font-size:.7rem;text-transform:uppercase;letter-spacing:.3em;color:var(--faint)}
</style>

<header class="intro">
  <h1>The one allowed data moment.</h1>
  <p>Scroll to draw the curve; the counter climbs with it. Illustrative only.</p>
</header>

<div class="spacer" aria-hidden="true"></div>

<main class="stage">
  <figure class="alpha-fig">
    <div class="alpha-head">
      <figcaption>Cumulative alpha · illustrative</figcaption>
      <p class="pv">Portfolio value: <b id="alpha-value">$100,000</b></p>
    </div>
    <svg viewBox="0 0 1200 280" role="img" aria-label="An upward-trending cumulative alpha curve, illustrative">
      <defs>
        <linearGradient id="hf-alpha-stroke" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%" stop-color="#c9a24b" stop-opacity="0.25"></stop>
          <stop offset="100%" stop-color="#e7cd8c"></stop>
        </linearGradient>
      </defs>
      <line x1="0" y1="252" x2="1200" y2="252" stroke="#2a2519" stroke-width="1"></line>
      <path id="alpha-path" d="M0,250 C150,250 180,212 260,206 C340,200 360,172 460,152 C560,132 580,150 660,122 C760,84 800,112 900,82 C1000,56 1050,42 1200,22" fill="none" stroke="url(#hf-alpha-stroke)" stroke-width="2.5" stroke-linecap="round"></path>
    </svg>
    <p class="alpha-foot">Acct: hallowed finance trader</p>
  </figure>
</main>

<div class="spacer" aria-hidden="true"></div>

<script>
gsap.registerPlugin(ScrollTrigger);
const START_VALUE = 100000, END_VALUE = 1000000;
const formatUsd = v => `$${Math.round(v).toLocaleString('en-US')}`;
const path = document.getElementById('alpha-path');
const valueEl = document.getElementById('alpha-value');
const len = path.getTotalLength();
path.style.strokeDasharray = len;

gsap.matchMedia()
  .add('(prefers-reduced-motion: no-preference)', () => {
    path.style.strokeDashoffset = len;
    valueEl.textContent = formatUsd(START_VALUE);
    const counter = { v: START_VALUE };
    const tl = gsap.timeline({
      defaults: { ease: 'none' },
      scrollTrigger: { trigger: path, start: 'top 88%', end: 'bottom 62%', scrub: true },
    });
    tl.to(path, { strokeDashoffset: 0 }, 0);
    tl.to(counter, {
      v: END_VALUE,
      onUpdate: () => { valueEl.textContent = formatUsd(counter.v); },
    }, 0);
    return () => tl.scrollTrigger && tl.scrollTrigger.kill();
  })
  .add('(prefers-reduced-motion: reduce)', () => {
    path.style.strokeDashoffset = 0;
    valueEl.textContent = formatUsd(END_VALUE);
  });
</script>