<div class="timeline">
  <div class="timeline__grid">
    <div class="timeline__item">
      <div class="timeline__content">-1-</div>
      <div class="timeline__number">1</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-2-</div>
      <div class="timeline__number">2</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-4-</div>
      <div class="timeline__number">4</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-3-</div>
      <div class="timeline__number">3</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-5-</div>
      <div class="timeline__number">5</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-6-</div>
      <div class="timeline__number">6</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-7-</div>
      <div class="timeline__number">7</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-8-</div>
      <div class="timeline__number">8</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-9-</div>
      <div class="timeline__number">9</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-10-</div>
      <div class="timeline__number">10</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-11-</div>
      <div class="timeline__number">11</div>
    </div>
    <div class="timeline__item">
      <div class="timeline__content">-12-</div>
      <div class="timeline__number">12</div>
    </div>
  </div>

  <svg class="svg-timeline" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <linearGradient id="grad" x1="0" y1="0" x2="100%" y2="100%" >
          <stop offset="0%"  stop-color="#d73370"/>
          <stop offset="100%" stop-color="#716ca3"/>
      </linearGradient>
    </defs>
    <path class="svg-timeline-static" fill="none" stroke="url(#grad)" stroke-width="2" stroke-dasharray="5" d="" />
    <path class="svg-timeline-animatable" fill="none" stroke="url(#grad)" stroke-width="3" d="" />
  </svg>
</div>
* {
  box-sizing: border-box;
}

body {
  height: 5000px;
  background-color: #0a0816;
}

.timeline {
  position: fixed;
  margin: 50px;
  width: 75%;
  display: flex;
  flex-direction: column;
  text-align: center;
}

.timeline__grid {
  display: flex;
  flex-wrap: wrap;
  margin-top: -100px;
  margin-left: -10px;
  margin-right: -10px;
}

.timeline__item {
  position: relative;
  width: calc(33.33% - 20px);
  margin-top: 100px;
  margin-left: 10px;
  margin-right: 10px;
}

/* 
.timeline__item:nth-child(2n) {
  margin-right: calc(50% - 200px);
  margin-left: calc(50% - 150px);
}

.timeline__item:nth-child(4n-1) {
  margin-left: 100px;
} */

.timeline__content {
  background-color: #673ab7;
  color: #fff;
  height: 100px;
  margin-bottom: 20px;
  transition: background-color 250ms;
}

.timeline__number {
  position: absolute;
  left: calc(50% - 15px);
  bottom: -15px;
  width: 30px;
  height: 30px;
  line-height: 30px;
  background-color: #2196f3;
  border-radius: 50%;
  color: #fff;
  transition: background-color 250ms;
}

.timeline__item--active .timeline__number {
  background-color: #e91e63;
}

.timeline__item--active .timeline__content {
  background-color: #4caf50;
}

.svg-timeline {
  position: absolute;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: -1;
}
class Timeline {
  constructor(options) {
    this.svg = options.svg;
    this.cols = options.cols || 2;
    this.padding = options.padding || { x: 10, y: 10 };
    this.bevel = options.bevel || { x: 10, y: 10 };
    this.pivot = options.pivot || { x: 0.5, y: 0.5 };
    this.intesectOffset = options.intesectOffset || { x: 2, y: 2 };
    this.els = options.els || [];

    this.items = this.els.map(el => ({ el }));
    this.pathStatic = this.svg.querySelector('.svg-timeline-static');
    this.pathAnimatable = this.svg.querySelector('.svg-timeline-animatable');

    this.totalLength = 0;
    this.animProgress = 0;

    this.init();
  }
  
  init() {
    this.reorderItems();
    this.updateItems();
    this.draw();
    this.setProgress(0);
  }
  
  reorderItems() {
    for (let i = 0, ri = 0, row; i < this.items.length; i += this.cols, ri++) {
      if (ri % 2 === 0) {
        row = this.items.slice(i, i + this.cols);
        row[row.length - 1].last = true;
        row.forEach(item => item.reversed = false);
      }
      else {
        row = this.items.slice(i, i + this.cols).reverse();
        row[row.length - 1].last = true;
        row.forEach(item => item.reversed = true);
      }
      
      this.items.splice(i, this.cols, ...row);
    }
  }

  updateItems() {
    const svgRect = this.svg.getBoundingClientRect();
    this.svgRect = svgRect;

    this.items.forEach(item => {
      const rect = item.el.getBoundingClientRect();
      const point = {
        x: rect.left - svgRect.left + rect.width * this.pivot.x,
        y: rect.top - svgRect.top + rect.height * this.pivot.y,
      };
      
      item.point = point;
    });
  }
  
  draw() {    
    let d = `M ${this.items[0].point.x} ${this.items[0].point.y}`;
 
    for (let i = 1, item; i < this.items.length - 1; i++) {
      item = this.items[i];
      d += ` L ${item.point.x} ${item.point.y}`;

      if (item.last) {
        let toX = item.reversed ?
          this.bevel.x - this.padding.x :
          this.svgRect.width - this.bevel.x + this.padding.x;
        let toY = item.point.y;
        d += ` L ${toX} ${toY}`;
        
        toX = item.reversed ?
          -this.padding.x :
          this.svgRect.width + this.padding.x;
        toY = item.point.y + this.bevel.y;
        
        const sweepFlag = item.reversed ? 0 : 1;
        d += ` A ${this.bevel.x} ${this.bevel.y} 0 0 ${sweepFlag} ${toX} ${toY}`;
        
        toY = this.items[i + 1].point.y - this.bevel.y;
        d += ` L ${toX} ${toY}`;
        
        toX = item.reversed ?
          -this.padding.x + this.bevel.x:
          this.svgRect.width + this.padding.x - this.bevel.x;
        toY = this.items[i + 1].point.y;
        d += ` A ${this.bevel.x} ${this.bevel.y} 0 0 ${sweepFlag} ${toX} ${toY}`;
      }
    }
    
    const last = this.items[this.items.length - 1];
    d += ` L ${last.point.x} ${last.point.y}`;
    
    this.pathStatic.setAttribute('d', d);
    this.pathAnimatable.setAttribute('d', d);
  }

  setProgress(p) {
    this.animProgress = p;
    this.totalLength = this.pathStatic.getTotalLength();
    this.pathAnimatable.setAttribute('stroke-dashoffset', this.totalLength - this.totalLength * this.animProgress);
    this.pathAnimatable.setAttribute('stroke-dasharray', this.totalLength);

    this.checkIntersect();
  }
  
  checkIntersect() {
    const len = this.totalLength * this.animProgress;
    const step = Math.max(1, Math.min(this.intesectOffset.x, this.intesectOffset.y));
    
    let intersectIndex = -1;
    for (let l = len, point; l >= 0; l -= step) {
      point = this.pathAnimatable.getPointAtLength(l);
      intersectIndex = this.items.findIndex(item => {
        return (
          (point.x > item.point.x - this.intesectOffset.x) &&
          (point.x < item.point.x + this.intesectOffset.x) &&
          (point.y > item.point.y - this.intesectOffset.y) &&
          (point.y < item.point.y + this.intesectOffset.y)
        );
      });
      
      if (intersectIndex !== -1) break;
    }

    if (intersectIndex === -1) return;
    
    for (let i = 0; i <= intersectIndex; i++) {
      this.items[i].el.classList.add('timeline__item--active');
    }
    
    for (let i = intersectIndex + 1; i < this.items.length; i++) {
      this.items[i].el.classList.remove('timeline__item--active');
    }
  }
  
  resize() {
    this.updateItems();
    this.draw();
    this.setProgress(this.animProgress);
  }
}

const tl = new Timeline({
  cols: 3,
  els: Array.from(document.querySelectorAll('.timeline__item')),
  svg: document.querySelector('.svg-timeline'),
  pivot: { x: 0.5, y: 1 },
  bevel: { x: 35, y: 35 },
  padding: { x: 20, y: 20 },
  intesectOffset: { x: 15, y: 15 },
});

window.addEventListener('resize', () => tl.resize() );

window.addEventListener('scroll', () => {
  const dt = document.documentElement.scrollTop || document.body.scrollTop;
  const sh = document.documentElement.scrollHeight || document.body.scrollHeight;
  const scrolled = dt / (sh - document.documentElement.clientHeight);
  tl.setProgress(scrolled);
})

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.