Aduok Code

A Scroll-Traced Org Chart, Built from a Pinned Horizontal Canvas, Self-Drawing SVG Threads, and Claymorphic Role Cards

Introduction

This sketch reads as an org chart you walk through rather than glance at: a CEO node, three manager branches, and a base layer of developers and ICs, laid out left to right on a canvas that is roughly 2000px wide. The trick is that the page never actually scrolls sideways — a single pinned section converts the visitor’s ordinary vertical scroll into a horizontal scrub of that canvas, so the chart advances one wheel-tick at a time instead of jumping.

Four responsibilities stay separate the whole way through: the scroll rig only knows how to turn vertical scroll into a horizontal transform, the SVG threads only know how to draw themselves in as they enter view, the cards only know how to pop in and hold a claymorphic shadow pair, and a small pulse layer only knows how to loop along a thread once it is fully drawn. None of the four reach into each other’s internals.

What you will learn

How to pin a section and drive its horizontal position from vertical scroll with GSAP’s containerAnimation option — how to make an SVG path draw itself in with stroke-dasharray instead of a canvas particle trick — how to build a dark claymorphic card with two opposing box-shadows instead of the usual light-gray neumorphism default — and how to send a small circle looping along a finished path with MotionPathPlugin.

01
Part One
A Vertical Scroll Pinned into a Horizontal Scrub

Step 1 — Pin the Wrapper, Scrub the Canvas

The wrapper is a full-viewport section with overflow hidden; inside it sits a much wider canvas holding every node and connector. ScrollTrigger pins the wrapper in place and, as the page keeps scrolling underneath it, tweens the canvas’s x position from 0 to the negative of however much extra width it has beyond the viewport. The result feels like scrolling sideways, but it is really just one long vertical scroll being read as a horizontal value.

JS
const canvas = document.querySelector(".canvas");
let scrollMax = canvas.scrollWidth - window.innerWidth + 200;

const horizontalTween = gsap.to(canvas, {
  x: -scrollMax,
  ease: "none",
  scrollTrigger: {
    trigger: ".scroll-wrapper",
    pin: true,
    scrub: 1,
    end: () => "+=" + scrollMax,
    onUpdate: (self) => {
      gsap.set(".progress-fill", { scaleX: self.progress });
    }
  }
});

That same horizontalTween is passed into every other ScrollTrigger on the page as containerAnimation, which is what lets nodes and lines that live far to the right still animate in relative to when they scroll into the (still notionally vertical) viewport.

02
Part Two
Threads That Draw Themselves In

Step 2 — stroke-dasharray as a Progress Bar

Each connector between a role and its reports is a plain SVG path. Setting stroke-dasharray to the path’s own total length and stroke-dashoffset to that same value hides the whole line; animating dashoffset down to zero reveals it stroke by stroke. Because the tween is scrubbed against containerAnimation, the line only draws as far as the visitor has actually scrolled into it — scroll back and it retreats.

JS
const paths = gsap.utils.toArray(".line-path");

paths.forEach((path) => {
  const length = path.getTotalLength();
  gsap.set(path, { strokeDasharray: length, strokeDashoffset: length });

  gsap.to(path, {
    strokeDashoffset: 0,
    ease: "power2.inOut",
    scrollTrigger: {
      trigger: path,
      containerAnimation: horizontalTween,
      start: "left right-=180",
      end: "right center",
      scrub: true
    }
  });
});

The line doesn’t know it’s connecting a manager to a developer — it just knows how far along itself it’s allowed to be, given how far the visitor has scrolled.

03
Part Three
Claymorphic Role Cards, Not Flat Boxes

Step 3 — Two Shadows Instead of One

Every node — dot, plus-button, or card — shares one .clay base class carrying a pair of opposing box-shadows: a near-black cast shadow on one corner and a lighter raised tone on the other, both derived from the same background color. On a dark forest-ink background that reads as soft, pressed clay instead of the light-gray neumorphism this style usually defaults to. Cards additionally get a photo, a level badge, an icon, and two stat numbers pulled straight from the org data.

CSS
.clay {
  position: absolute;
  transform: translate(-50%, -50%);
  background: var(--raise);
  box-shadow:
    10px 10px 22px var(--shadow-dark),
    -8px -8px 18px var(--shadow-light);
}

.card.is-trigger,
.card.is-success {
  border-color: var(--brass);
}
Why the shape still reads as a hierarchy

The canvas itself has no notion of levels — it only positions absolutely-placed elements at fixed x/y coordinates. The hierarchy reads correctly because the CEO card sits alone on the center line, the three manager cards are spread vertically at a shared x, and every connector routes back to that shared center line before continuing to the base layer, the same way a hand-drawn org chart would.

04
Part Four
Pop-In Reveals and a Traveling Pulse

Step 4 — Reveal on Entry, Then Loop a Pulse

Every dot, button, and card carries a .gs-reveal class and pops in from scale 0 with a back-out ease as it scrolls into range. Once a connector finishes drawing, its scrollTrigger’s onEnter callback releases a small circle onto MotionPathPlugin, which loops it along that exact path indefinitely — a lightweight way to suggest something is actively flowing through the structure without re-triggering the draw animation itself.

JS
gsap.utils.toArray(".gs-reveal").forEach((el) => {
  gsap.from(el, {
    scale: 0,
    opacity: 0,
    duration: reduceMotion ? 0.01 : 0.8,
    ease: "back.out(1.5)",
    scrollTrigger: {
      trigger: el,
      containerAnimation: horizontalTween,
      start: "left right-=140",
      toggleActions: "play none none reverse"
    }
  });
});

// once a thread is fully drawn, release a looping pulse onto it
onEnter: () => {
  gsap.set(pulse, { opacity: 1 });
  gsap.to(pulse, {
    motionPath: { path, align: path, alignOrigin: [0.5, 0.5] },
    duration: 2.4,
    ease: "sine.inOut",
    repeat: -1
  });
}

Tuning Reference

PropertyExampleEffect
canvas width / height2000px / 900pxTotal scrub distance and vertical room for branch spread
branch y-offsets220 / 450 / 680How far the three manager cards spread above and below the center line
scrub value1How tightly the horizontal position and line-draw follow the scrollbar; higher adds lag
reveal start offsetleft right-=140How early a node pops in before it reaches the viewport edge
pulse duration2.4s, repeat -1Speed and looping of the traveling dot once a thread finishes drawing
prefers-reduced-motionskips pulse + shortens reveal durationKeeps the chart usable without relying on motion to convey structure

Full Source Code

The complete sketch is a single self-contained HTML document — the pinned scrub rig, the self-drawing SVG threads, the claymorphic card system, and the reveal/pulse choreography, with no build step or external diagramming library.

HTML — hierarchy-flow.html (complete)
// Full source available in the Aduok GitHub repository
// https://github.com/aduok/aduok-code-snippets/blob/main/blog/hierarchy-flow.html
Want to try it yourself?Open the live hierarchy-flow file