SVG route draw

A winding route draws itself down the page, and checkpoints light up as the line reaches them.

How it works

The path’s total length comes from getTotalLength(). It is used for stroke-dasharray, and scroll progress sets stroke-dashoffset. Each checkpoint stores its position along the path, and activates once the drawn length passes it.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SVG route draw</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }

body { background: #0b3d2e; color: #e6ffe9; }
.track { padding: 10vh 0 30vh; }
.map { display: block; width: min(90vw, 420px); margin: 0 auto; overflow: visible; }
.ghost { fill: none; stroke: rgba(230,255,233,.15); stroke-width: 4; stroke-dasharray: 2 10; stroke-linecap: round; }
.route { fill: none; stroke: #ffd166; stroke-width: 6; stroke-linecap: round; }
.pt circle { fill: #0b3d2e; stroke: rgba(230,255,233,.3); stroke-width: 4; transition: fill .3s, stroke .3s; }
.pt text { fill: #e6ffe9; font: 600 18px system-ui, sans-serif; opacity: .3; transition: opacity .3s; }
.pt.on circle { fill: #ffd166; stroke: #ffd166; }
.pt.on text { opacity: 1; }
</style>
</head>
<body>
<section class="track">
  <svg class="map" viewBox="0 0 400 1200" preserveAspectRatio="xMidYMin meet">
    <path class="ghost" d="M200 20 C 380 120, 20 220, 200 320 S 380 520, 200 620 S 20 820, 200 920 S 360 1120, 200 1180"/>
    <path class="route" id="route" d="M200 20 C 380 120, 20 220, 200 320 S 380 520, 200 620 S 20 820, 200 920 S 360 1120, 200 1180"/>
    <g id="pts"></g>
  </svg>
</section>
<script>
const route = document.getElementById('route');
const pts = document.getElementById('pts');
const L = route.getTotalLength();
route.style.strokeDasharray = L;
route.style.strokeDashoffset = L;
const names = ['Start', 'Bridge', 'Forest', 'Lake', 'Ridge', 'Summit'];
const marks = names.map((n, i) => {
  const at = (L * i) / (names.length - 1);
  const p = route.getPointAtLength(at);
  const side = p.x > 200 ? 18 : -18;
  pts.insertAdjacentHTML('beforeend', '<g class="pt"><circle cx="' + p.x + '" cy="' + p.y + '" r="11"/><text x="' + (p.x + side) + '" y="' + (p.y + 6) + '" text-anchor="' + (side > 0 ? 'start' : 'end') + '">' + n + '</text></g>');
  return { at, el: pts.lastElementChild };
});
function update() {
  const r = route.getBoundingClientRect();
  const p = Math.min(1, Math.max(0, (innerHeight * 0.6 - r.top) / r.height));
  const drawn = p * L;
  route.style.strokeDashoffset = L - drawn;
  marks.forEach(m => m.el.classList.toggle('on', drawn >= m.at - 1));
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More data effects