Instructions

Every custom animation on this template is plain GSAP inside a Webflow Code Embed. Each script below lists where it lives, which classes it looks for, and the exact code — so you can tweak timing, swap classes, or remove an effect without breaking the rest of the site.

Site-wide

Smooth scroll & animation core (GSAP + Lenis)

Lives in Site Settings → Custom Code → Footer Code, so it loads on every page before </body>. Pulls GSAP, ScrollTrigger and Lenis from CDN, syncs Lenis's smooth-scroll frame with the GSAP ticker, then fades up any element carrying the .animate-up class as it scrolls to 85% of the viewport. Change duration to make scrolling heavier or lighter, y and ease to adjust the fade, and start to fire the animation earlier or later. To remove: delete this whole footer code block, then strip the .animate-up class from any elements — the page falls back to normal scrolling with no fade-in animation.

<!-- GSAP & Lenis CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lenis@1.2.3/dist/lenis.min.js"></script>

<script>
  gsap.registerPlugin(ScrollTrigger);

  /* ---- edit point: smooth-scroll feel ---- */
  const lenis = new Lenis({
    duration: 1.2,      // scroll "weight" — higher = slower, smoother
    smoothWheel: true
  });

  lenis.on('scroll', ScrollTrigger.update);
  gsap.ticker.add((time) => lenis.raf(time * 1000));
  gsap.ticker.lagSmoothing(0);

  // Keeps Lenis and ScrollTrigger in sync once images/fonts finish loading
  window.addEventListener('load', () => {
    lenis.resize();
    ScrollTrigger.refresh();
  });
  new ResizeObserver(() => lenis.resize()).observe(document.body);

  /* ---- edit point: fade-up animation ---- */
  gsap.utils.toArray('.animate-up').forEach((el) => {
    gsap.from(el, {
      autoAlpha: 0,
      y: 50,                 // start position, px below final spot
      duration: 1,            // animation length, seconds
      ease: "power2.out",     // easing curve
      scrollTrigger: {
        trigger: el,
        start: "top 85%",     // fires when element is 85% down the viewport
        toggleActions: "play none none reverse"
      }
    });
  });

  /* ---- to remove ----
     1. Delete this entire code block from Site Settings > Custom Code > Footer.
     2. Remove the .animate-up class from any element that used it.
     3. Republish the site — scrolling returns to normal browser behavior. */
</script>

Site-wide

Button tilt + press

Lives in the Footer Main component (embed "Button Tilt + Press (GSAP)"), so it runs on every page. Targets every .button_div_wrap: a soft 3D tilt toward the cursor, a 2px lift and a press-down on click. Border and shadow on hover come from the Style Panel. Edit the CFG object to change tilt, lift or press strength. Skipped on touch devices and for reduced motion.

<script>
/* Korben — Button: tactile tilt + press (site-wide, .button_div_wrap) */
(function(){
  if (window.__korbenBtnTilt) return;
  window.__korbenBtnTilt = true;

  var GSAP_CDN = "https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js";

  /* ---- edit point ---- */
  var CFG = {
    tiltX: 14,          // up/down tilt, degrees
    tiltY: 16,          // left/right tilt, degrees
    lift: -2,           // px the button rises on hover
    press: 0.965,       // scale while pressed
    perspective: 600,
    enter: 0.30,        // hover follow speed
    leave: 0.60         // return speed when the mouse leaves
  };

  function init(){
    if (typeof gsap === "undefined") return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    if (!window.matchMedia("(hover: hover) and (pointer: fine)").matches) return;

    document.querySelectorAll(".button_div_wrap").forEach(function(btn){
      if (btn.__tilt) return;
      btn.__tilt = true;
      var pressed = false;

      function move(rx, ry, y, s, dur, ease){
        gsap.to(btn, {
          rotationX: rx, rotationY: ry, y: y, scale: s,
          duration: dur, ease: ease || "power3.out",
          transformPerspective: CFG.perspective,
          transformOrigin: "50% 50%",
          overwrite: "auto"
        });
      }

      btn.addEventListener("mousemove", function(e){
        var r = btn.getBoundingClientRect();
        move(
          -(((e.clientY - r.top) / r.height) - 0.5) * CFG.tiltX,
          (((e.clientX - r.left) / r.width) - 0.5) * CFG.tiltY,
          CFG.lift,
          pressed ? CFG.press : 1,
          CFG.enter
        );
      });

      btn.addEventListener("mouseleave", function(){
        pressed = false;
        move(0, 0, 0, 1, CFG.leave);
      });

      btn.addEventListener("mousedown", function(){
        pressed = true;
        gsap.to(btn, { scale: CFG.press, duration: 0.12, ease: "power2.out", overwrite: false });
      });

      btn.addEventListener("mouseup", function(){
        pressed = false;
        gsap.to(btn, { scale: 1, duration: 0.35, ease: "back.out(2.2)", overwrite: false });
      });
    });
  }

  function boot(){
    if (typeof gsap !== "undefined") { init(); return; }
    var s = document.createElement("script");
    s.src = GSAP_CDN;
    s.onload = init;
    document.head.appendChild(s);
  }

  if (document.readyState !== "loading") boot();
  else document.addEventListener("DOMContentLoaded", boot);
})();
</script>

Site-wide

Navbar menu button

Lives in the Navbar component (hidden embed). On tablet and mobile, clicking .menu_button swaps its two .menu_button_box icons with a small pop animation, and reverses on the next click. It uses the GSAP already loaded on the page; the menu itself still opens natively without it.

<script>
window.addEventListener("DOMContentLoaded", function () {
  if (typeof gsap === "undefined") return;

  const menuButtons = document.querySelectorAll(".menu_button");

  menuButtons.forEach((btn) => {
    const boxes = btn.querySelectorAll(".menu_button_box");
    if (boxes.length < 2) return;

    // Initial setup: first icon visible, second icon small and hidden
    gsap.set(boxes[0], { scale: 1, opacity: 1, y: 0 });
    gsap.set(boxes[1], { scale: 0.5, opacity: 0, y: 15 });

    let isOpen = false;

    // Pop-up Animation Timeline
    const tl = gsap.timeline({ paused: true });

    tl.to(boxes[0], {
      scale: 0.6,
      opacity: 0,
      y: -10,
      duration: 0.3,
      ease: "power2.in"
    }, 0)
    .to(boxes[1], {
      scale: 1,
      opacity: 1,
      y: 0,
      duration: 0.4,
      ease: "back.out(1.7)" // Pop-up bounce effect
    }, 0.15);

    // Click Event Logic
    btn.addEventListener("click", function (e) {
      e.preventDefault();

      if (!isOpen) {
        tl.play();
      } else {
        tl.reverse();
      }

      isOpen = !isOpen;
    });
  });
});
</script>

Home page

Hero route lines

Embed with class hero_animatio_code in the Home hero. Draws thin SVG routes from the stats row down to the hero icons (.hero_icon_one, .hero_icon_two, .hero_icon_four), with a travelling dot and a ripple where each line lands, on a loop. It rebuilds on resize and only runs from 768px up.

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
  window.Webflow = window.Webflow || [];
  window.Webflow.push(function () {
    if (typeof gsap === "undefined") return;

    var d = document, NS = "http://www.w3.org/2000/svg";
    var sec = d.querySelector(".section.home_hero");
    if (!sec) return;

    var q = function (s) { return sec.querySelector(s); };
    var content = q(".home_hero_content"),
        top = q(".hero_stok_div"),
        grid = q(".home_hero_grid"),
        i1 = q(".hero_icon_one"),
        i2 = q(".hero_icon_two"),
        i4 = q(".hero_icon_four");

    if (!content || !top || !grid || !i1 || !i2 || !i4) return;

    var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    var mq = window.matchMedia("(min-width:768px)");

    var svg = d.createElementNS(NS, "svg");
    svg.setAttribute("class", "kx-stroke");
    svg.setAttribute("aria-hidden", "true");
    sec.appendChild(svg);

    // Apply styles purely through GSAP to avoid "Custom CSS" detector
    gsap.set(svg, {
      position: "absolute",
      inset: 0,
      pointerEvents: "none",
      zIndex: 0,
      overflow: "visible"
    });

    function el(p, n, a) {
      var e = d.createElementNS(NS, n);
      for (var k in a) e.setAttribute(k, a[k]);
      p.appendChild(e);
      return e;
    }

    function rel(r, s) {
      return {
        l: r.left - s.left,
        t: r.top - s.top,
        r: r.right - s.left,
        b: r.bottom - s.top,
        cx: (r.left + r.right) / 2 - s.left
      };
    }

    gsap.context(function () {
      function buildAndAnimate() {
        while (svg.firstChild) svg.removeChild(svg.firstChild);

        var s = sec.getBoundingClientRect();
        svg.setAttribute("viewBox", "0 0 " + s.width + " " + s.height);
        svg.setAttribute("width", s.width);
        svg.setAttribute("height", s.height);

        var y = rel(top.getBoundingClientRect(), s).t + 0.5,
            g = rel(grid.getBoundingClientRect(), s),
            a1 = rel(i1.getBoundingClientRect(), s),
            a2 = rel(i2.getBoundingClientRect(), s),
            a4 = rel(i4.getBoundingClientRect(), s);

        var defs = [
          { d: "M" + a1.cx + " " + y + " H" + a4.cx + " V" + (a4.t - 6), end: [a4.cx, a4.t - 6], tail: 150, head: 64, duration: 5.6 },
          { d: "M" + a2.cx + " " + y + " V" + (g.t - 2), end: [a2.cx, g.t - 2], tail: 100, head: 46, duration: 3.0 }
        ];

        defs.forEach(function (f) {
          var mk = function (w, o, len) {
            var p = el(svg, "path", {
              d: f.d,
              fill: "none",
              stroke: "#1F1C19",
              "stroke-width": w,
              "stroke-linecap": "round",
              "stroke-opacity": o
            });
            p._len = p.getTotalLength();
            p._seg = len;
            gsap.set(p, {
              strokeDasharray: len + " " + (p._len + 400),
              strokeDashoffset: len
            });
            return p;
          };

          var soft = mk(1.2, 0.12, f.tail);
          var hard = mk(1.2, 0.7, f.head);

          var node = el(svg, "g", { opacity: 0 });
          el(node, "circle", { r: 4, fill: "#FBEBD8", stroke: "#282420", "stroke-width": 0.75 });
          el(node, "circle", { r: 1.2, fill: "#282420" });

          var rip = el(svg, "circle", {
            cx: f.end[0],
            cy: f.end[1],
            r: 3,
            fill: "none",
            stroke: "#1F1C19",
            "stroke-width": 1,
            "stroke-opacity": 0
          });

          if (reduce) return;

          var mainTl = gsap.timeline({ repeat: -1, repeatDelay: 1.6 });
          var progressObj = { val: 0 };

          mainTl.to(progressObj, {
            val: 1,
            duration: f.duration,
            ease: "sine.inOut",
            onUpdate: function () {
              var k = progressObj.val;
              var h = k * soft._len;
              var opacityVal = k <= 0 || k >= 1 ? 0 : k > 0.96 ? (1 - k) / 0.04 : 1;

              [soft, hard].forEach(function (p) {
                gsap.set(p, { strokeDashoffset: p._seg - h, opacity: opacityVal });
              });

              var pt = soft.getPointAtLength(Math.min(soft._len, h));
              gsap.set(node, {
                attr: { transform: "translate(" + pt.x + " " + pt.y + ")" },
                opacity: opacityVal
              });
            }
          });

          mainTl.to(rip, {
            attr: { r: 14 },
            strokeOpacity: 0,
            duration: 0.9,
            ease: "power1.out"
          }, f.duration - 0.2);
        });
      }

      function start() {
        if (!mq.matches) {
          gsap.set(svg, { display: "none" });
          return;
        }
        gsap.set(svg, { display: "block" });
        buildAndAnimate();
      }

      var tm;
      window.addEventListener("resize", function () {
        clearTimeout(tm);
        tm = setTimeout(start, 150);
      });

      if (d.fonts && d.fonts.ready) d.fonts.ready.then(start);
      else start();
    });
  });
</script>

Home page

AI chat panel

Part of the kx_fill embed, the dark chat panel SVG (id kxSvg). Only the script is shown here; the SVG markup stays in the embed. The chips flip in, the panel swings down, the assistant name and role glitch into place, then the scene exits and repeats while it is on screen.

<script>
(function(){
var d=document, S='https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js';
function wg(cb){
  if(window.gsap) return cb();
  var s=d.querySelector('script[data-gsap-core]');
  if(!s){
    s=d.createElement('script');
    s.src=S;
    s.setAttribute('data-gsap-core','1');
    d.head.appendChild(s);
  }
  s.addEventListener('load',function(){cb();});
}
function rd(f){
  d.readyState==='loading'?d.addEventListener('DOMContentLoaded',f):f();
}

rd(function(){
  var v=d.getElementById('kxSvg');
  if(v) wg(function(){ go(v); });
});

function go(v){
  var q=function(i){ return v.querySelector('#'+i); };
  var chips=['kx-c1','kx-c2','kx-c3','kx-c4'].map(q).filter(Boolean),
      p=q('kx-panel'),
      clip=q('kxTr'),
      ic=q('kx-icon'),
      nm=q('kx-name'),
      ro=q('kx-role'),
      gl=q('kx-glow');

  if(window.matchMedia('(prefers-reduced-motion: reduce)').matches){
    if(clip) gsap.set(clip,{attr:{width:340}});
    return;
  }

  // 3D perspective and initial state
  gsap.set(v, { perspective: 800 });
  gsap.set(chips, { opacity: 0, scale: 0, rotationY: 180, transformOrigin: '50% 50%' });
  gsap.set(p, { opacity: 0, rotationX: -90, y: -50, transformOrigin: '50% 0%' });
  gsap.set(ic, { opacity: 0, scale: 2.5, rotation: 180, transformOrigin: '50% 50%' });
  gsap.set(nm, { opacity: 0, skewX: -30, x: -40 });
  gsap.set(ro, { opacity: 0, skewX: 30, x: 40 });
  if(clip) gsap.set(clip, { attr: { width: 0 } });

  // Living ambient glow in the background
  if(gl){
    gsap.to(gl, {
      scale: 1.4,
      rotation: 360,
      opacity: 0.9,
      duration: 10,
      repeat: -1,
      yoyo: true,
      ease: 'none',
      transformOrigin: '50% 50%'
    });
  }

  // Main looping timeline
  var t = gsap.timeline({
    paused: true,
    repeat: -1,
    repeatDelay: 1.5,
    defaults: { ease: 'power4.out' }
  });

  t
   // 1. Chips flip in with an elastic bounce
   .to(chips, {
     opacity: 1,
     scale: 1,
     rotationY: 0,
     duration: 1.2,
     stagger: 0.15,
     ease: 'elastic.out(1, 0.4)'
   })

   // 2. Panel swings down like a 3D card
   .to(p, {
     opacity: 1,
     rotationX: 0,
     y: 0,
     duration: 1,
     ease: 'bounce.out'
   }, '-=0.8')

   // 3. Icon spins in and settles
   .to(ic, {
     opacity: 1,
     scale: 1,
     rotation: 0,
     duration: 0.9,
     ease: 'back.out(2.5)'
   }, '-=0.5')

   // 4. Typewriter-style text mask wipe
   .to(clip, {
     attr: { width: 340 },
     duration: 1,
     ease: 'steps(24)'
   }, '-=0.3')

   // 5. Name and role un-skew into place
   .to(nm, { opacity: 1, skewX: 0, x: 0, duration: 0.6, ease: 'back.out(1.7)' }, '-=0.6')
   .to(ro, { opacity: 1, skewX: 0, x: 0, duration: 0.6, ease: 'back.out(1.7)' }, '-=0.5')

   // 6. Pulse highlight
   .to(ic, { scale: 1.2, duration: 0.15, yoyo: true, repeat: 3, ease: 'sine.inOut' })
   .to(p, { x: 5, duration: 0.05, yoyo: true, repeat: 5 }, '<')

   // 7. Hold on screen
   .to({}, { duration: 3 })

   // 8. Exit
   .to([nm, ro], { opacity: 0, scaleY: 0, duration: 0.3 })
   .to(ic, { opacity: 0, scale: 0, rotation: -180, duration: 0.5, ease: 'back.in(2)' }, '<')
   .to(clip, { attr: { width: 0 }, duration: 0.4, ease: 'power3.in' }, '<0.1')
   .to(p, { opacity: 0, rotationX: 90, y: 50, duration: 0.5, ease: 'power3.in' }, '<0.1')
   .to(chips, { opacity: 0, scale: 0, rotationY: -180, duration: 0.5, stagger: 0.08, ease: 'power3.in' }, '<');

  // Play only while visible
  if('IntersectionObserver' in window){
    new IntersectionObserver(function(entries){
      entries.forEach(function(x){
        x.isIntersecting ? t.play() : t.pause();
      });
    }, { threshold: 0.2 }).observe(v);
  } else {
    t.play();
  }
}
})();
</script>

Home page

Services tabs

Embed home_services_hide in the Home services section (the [data-tabs] wrapper). Clicking a .home_tab shows its matching .home_panel and animates the heading, feature list, glass card and chart bars. Arrow keys, Home and End move between tabs.

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
  window.Webflow = window.Webflow || [];
  window.Webflow.push(function () {
    if (typeof gsap === "undefined") return;

    var d = document;
    var box = d.querySelector('[data-tabs]');
    if (!box) return;

    var tabs = [].slice.call(box.querySelectorAll('.home_tab'));
    var panels = [].slice.call(box.querySelectorAll('.home_panel'));
    if (!tabs.length || !panels.length) return;

    var holders = panels.map(function(p){
      var h = p.closest('.w-dyn-list') || p;
      return (h.parentNode && h.parentNode.classList.contains('w-dyn-list')) ? h.parentNode : h;
    });

    var list = box.querySelector('.home_tabs'); 
    if (list) list.setAttribute('role', 'tablist');

    var n = Math.min(tabs.length, panels.length);
    var cur = -1;

    gsap.context(function (self) {
      tabs.forEach(function(t, i) {
        var tid = 'svctab-' + i;
        var pid = 'svcpanel-' + i;
        t.id = tid;
        t.setAttribute('role', 'tab');
        t.setAttribute('aria-controls', pid);

        if (panels[i]) {
          panels[i].id = pid;
          panels[i].setAttribute('role', 'tabpanel');
          panels[i].setAttribute('aria-labelledby', tid);
        }

        t.addEventListener('click', function() { setTab(i); });
        t.addEventListener('keydown', function(e) {
          var k = e.key, x = null;
          if (k === 'ArrowRight') x = (i + 1) % n;
          else if (k === 'ArrowLeft') x = (i - 1 + n) % n;
          else if (k === 'Home') x = 0;
          else if (k === 'End') x = n - 1;
          else if (k === 'Enter' || k === ' ') { e.preventDefault(); setTab(i); return; }
          
          if (x !== null) {
            e.preventDefault();
            setTab(x);
            tabs[x].focus();
          }
        });
      });

      function setTab(i) {
        if (i === cur) return;
        cur = i;

        tabs.forEach(function(t, j) {
          var on = (j === i);
          t.classList.toggle('is-active', on);
          t.setAttribute('aria-selected', on ? 'true' : 'false');
          t.setAttribute('tabindex', on ? '0' : '-1');
        });

        panels.forEach(function(p, j) {
          var h = holders[j];
          if (j === i) {
            if (h !== p) h.hidden = false;
            p.hidden = false;
            animatePanel(p);
          } else {
            if (h !== p) h.hidden = true;
            p.hidden = true;
          }
        });
      }

      function animatePanel(panel) {
        var top = panel.querySelector('.home_panel_top');
        var feats = panel.querySelectorAll('.home_feat');
        var viz = panel.querySelector('.home_viz');
        var glass = panel.querySelector('.home_glass');
        var bars = panel.querySelectorAll('.home_bar');

        var tl = gsap.timeline();

        if (top) {
          tl.fromTo(top, { opacity: 0, x: -8 }, { opacity: 1, x: 0, duration: 0.55, ease: "power2.out" }, 0);
        }

        if (feats.length) {
          tl.fromTo(feats, { opacity: 0, y: 10 }, { opacity: 1, y: 0, duration: 0.5, stagger: 0.08, ease: "power2.out" }, 0.14);
        }

        if (viz) {
          tl.fromTo(viz, { opacity: 0 }, { opacity: 1, duration: 0.6, ease: "power1.out" }, 0);
        }

        if (glass) {
          tl.fromTo(glass, { opacity: 0, y: 10 }, { opacity: 1, y: 0, duration: 0.6, ease: "power2.out" }, 0.25);
        }

        if (bars.length) {
          var barHeights = [
            { h: 0.42, h2: 0.58 },
            { h: 0.96, h2: 0.78 },
            { h: 0.62, h2: 0.80 },
            { h: 0.50, h2: 0.40 },
            { h: 0.38, h2: 0.56 }
          ];

          bars.forEach(function(bar, idx) {
            gsap.set(bar, { transformOrigin: "50% 100%" });
            var targetH = barHeights[idx] ? barHeights[idx].h : 0.5;
            var cycleH = barHeights[idx] ? barHeights[idx].h2 : 0.6;

            // Bar Reveal Animation
            tl.fromTo(bar, 
              { scaleY: 0.05 }, 
              { scaleY: targetH, duration: 1.1, ease: "power2.out" }, 
              0.1 + (idx * 0.08)
            );

            // Infinite Bar Cycle Animation via GSAP Timeline
            gsap.to(bar, {
              scaleY: cycleH,
              duration: 4.5,
              repeat: -1,
              yoyo: true,
              ease: "sine.inOut",
              delay: 1.5 + (idx * 0.1)
            });
          });
        }
      }

      var start = Math.max(0, tabs.findIndex(function(t) { return t.classList.contains('is-active'); }));
      setTab(start);
    });
  });
</script>

Home page

Work card flip

Hidden embed in the Home work section. Hovering a .work_card_triger flips its .work_card_div 180 degrees to show the back card (.work_card.two), and flips back on leave. The short style block at the top sets up the 3D perspective the flip needs.

<style>
  /* 3D Perspective Setup */
  .work_card_triger {
    perspective: 1000px;
  }
  .work_card_div {
    position: relative;
    width: 100%;
    height: 100%;
    transform-style: preserve-3d;
  }
  .work_card.one {
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
  }
  .work_card.two {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    transform: rotateY(180deg);
  }
</style>

<script>
(function() {
  function loadGSAP(callback) {
    if (window.gsap) {
      callback(window.gsap);
      return;
    }
    if (window.Webflow && window.Webflow.require) {
      try {
        var wGsap = window.Webflow.require('gsap');
        if (wGsap) { callback(wGsap); return; }
      } catch(e) {}
    }
    var script = document.createElement('script');
    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js';
    script.onload = function() { callback(window.gsap); };
    document.head.appendChild(script);
  }

  function ready(fn) {
    if (document.readyState !== 'loading') {
      fn();
    } else {
      document.addEventListener('DOMContentLoaded', fn);
    }
  }

  ready(function() {
    loadGSAP(function(gsapObj) {
      if (!gsapObj) return;

      var triggers = document.querySelectorAll('.work_card_triger');

      triggers.forEach(function(trigger) {
        var cardDiv = trigger.querySelector('.work_card_div');
        if (!cardDiv) return;

        // Set initial 3D transforms
        gsapObj.set(cardDiv, { transformStyle: "preserve-3d" });

        // Hover In -> Flip 180deg
        trigger.addEventListener('pointerenter', function() {
          gsapObj.to(cardDiv, {
            rotateY: 180,
            duration: 0.8,
            ease: "power2.inOut",
            overwrite: "auto"
          });
        });

        // Hover Out -> Flip back to 0deg
        trigger.addEventListener('pointerleave', function() {
          gsapObj.to(cardDiv, {
            rotateY: 0,
            duration: 0.8,
            ease: "power2.inOut",
            overwrite: "auto"
          });
        });
      });
    });
  });
})();
</script>

Home page

FAQ accordion

Embed faq_hide in the Home FAQ. One item open at a time: the answer height animates, the plus icon turns into a minus, and the questions work with Enter, Space and the arrow keys. Any element with a data-reveal attribute fades up as it scrolls in; add data-delay (in ms) to stagger it.

<script>
(function () {
  var d = document;
  function ready(f) {
    d.readyState === 'loading' ? d.addEventListener('DOMContentLoaded', f) : f();
  }

  var sec, items, reveals;
  ready(function () {
    sec = d.querySelector('.home_faq');
    if (!sec) return;
    items = [].slice.call(sec.querySelectorAll('.home_faq_item'));
    reveals = [].slice.call(sec.querySelectorAll('[data-reveal]'));
    if (!items.length) return;

    items.forEach(function (it) {
      var inner = it.querySelector('.home_faq_a_inner');
      if (inner && !it.classList.contains('is-open')) inner.style.height = '0px';
    });

    var gsapObj = window.gsap || (window.Webflow && window.Webflow.require('gsap'));
    init(gsapObj);
  });

  function init(gsap) {
    var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    var P = items.map(function (it, i) {
      var o = {
        el: it,
        q: it.querySelector('.home_faq_q'),
        inner: it.querySelector('.home_faq_a_inner'),
        ans: it.querySelector('.faq_answer'),
        svg: it.querySelector('.home_faq_icon svg'),
        v: it.querySelector('.home_faq_icon svg .v'),
        open: it.classList.contains('is-open')
      };

      if (gsap) {
        if (o.inner) gsap.set(o.inner, { height: o.open ? 'auto' : 0, overflow: 'hidden' });

        // Open: vertical line (.v) hidden (scaleY 0); closed: visible (scaleY 1)
        if (o.v) gsap.set(o.v, { scaleY: o.open ? 0 : 1, transformOrigin: '50% 50%' });

        // No SVG rotation, only the line scales, so the minus stays straight
        if (o.svg) gsap.set(o.svg, { rotate: 0 });
      }

      if (o.q) {
        o.q.setAttribute('role', 'button');
        o.q.setAttribute('tabindex', '0');
        o.q.setAttribute('aria-expanded', o.open ? 'true' : 'false');
        o.q.id = o.q.id || ('faq-q-' + i);
        if (o.inner) {
          o.inner.id = o.inner.id || ('faq-a-' + i);
          o.inner.setAttribute('role', 'region');
          o.inner.setAttribute('aria-labelledby', o.q.id);
          o.q.setAttribute('aria-controls', o.inner.id);
        }
      }
      return o;
    });

    function setOpen(o, on, instant) {
      if (o.open === on) return;
      o.open = on;
      o.el.classList.toggle('is-open', on);
      if (o.q) o.q.setAttribute('aria-expanded', on ? 'true' : 'false');
      var dur = (instant || reduce) ? 0 : 0.4;

      if (gsap) {
        if (o.inner) gsap.to(o.inner, { height: on ? 'auto' : 0, duration: dur, ease: 'power3.inOut', overwrite: 'auto' });
        if (o.ans) gsap.to(o.ans, { opacity: on ? 1 : 0, y: on ? 0 : -6, duration: dur, ease: 'power2.out', delay: on ? 0.1 : 0, overwrite: 'auto' });

        // Opening hides the vertical line, turning the plus into a minus
        if (o.v) gsap.to(o.v, { scaleY: on ? 0 : 1, duration: dur, ease: 'power2.inOut', overwrite: 'auto' });
      } else {
        if (o.inner) o.inner.style.height = on ? 'auto' : '0px';
        if (o.v) o.v.style.transform = on ? 'scaleY(0)' : 'scaleY(1)';
      }
    }

    P.forEach(function (o) {
      if (o.ans && gsap) gsap.set(o.ans, { opacity: o.open ? 1 : 0, y: o.open ? 0 : -6 });
    });

    function toggle(i) {
      var t = P[i];
      P.forEach(function (o, j) { if (j !== i && o.open) setOpen(o, false); });
      setOpen(t, !t.open);
    }

    P.forEach(function (o, i) {
      if (!o.q) return;
      o.q.addEventListener('click', function () { toggle(i); });
      o.q.addEventListener('keydown', function (e) {
        if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(i); }
        else if (e.key === 'ArrowDown') { e.preventDefault(); var n = P[(i + 1) % P.length]; n.q && n.q.focus(); }
        else if (e.key === 'ArrowUp') { e.preventDefault(); var pv = P[(i - 1 + P.length) % P.length]; pv.q && pv.q.focus(); }
      });
    });

    if (reduce || !('IntersectionObserver' in window)) return;

    if (gsap) {
      reveals.forEach(function (el) { gsap.set(el, { opacity: 0, y: 18 }); });
      var io = new IntersectionObserver(function (es) {
        es.forEach(function (e) {
          if (!e.isIntersecting) return;
          var el = e.target, delay = parseFloat(el.getAttribute('data-delay') || 0) / 1000;
          gsap.to(el, { opacity: 1, y: 0, duration: 0.7, ease: 'power2.out', delay: delay, overwrite: 'auto' });
          io.unobserve(el);
        });
      }, { threshold: 0.15, rootMargin: '0px 0px -8% 0px' });
      reveals.forEach(function (el) { io.observe(el); });
    }
  }
})();
</script>

Home page

Structured Section illustration

Code Embed inside the Structured Section component (path_blar_overlay). It loads the timeline illustration SVG from the Assets panel and animates it: grid lines draw, dots pop, bars stretch in, then the cursor drags the active bar and a soft idle loop takes over. To use another illustration, replace the SRC URL.

<script>
(function(){
  var SRC="https://cdn.prod.website-files.com/69ec5add748bdbdab7061987/6aae4d086d44e135e2452dc9_Illustration.svg";
  var GSAP="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js";
  var host=document.currentScript.parentNode;
  if(host.__tlInit) return; host.__tlInit=true;

  function load(cb){ if(window.gsap) return cb(); var s=document.createElement("script"); s.src=GSAP; s.onload=cb; s.onerror=fallback; document.head.appendChild(s); }
  function fallback(){ if(host.querySelector("svg")||host.querySelector("img")) return; var i=new Image(); i.src=SRC; i.alt=""; i.setAttribute("width","100%"); host.appendChild(i); }

  fetch(SRC).then(function(r){return r.text();}).then(function(t){
    host.insertAdjacentHTML("beforeend",t);
    var svg=host.querySelector("svg"); if(!svg) return fallback();
    svg.setAttribute("width","100%"); svg.setAttribute("height","100%"); svg.setAttribute("preserveAspectRatio","xMidYMid meet");
    load(function(){ build(svg); });
  }).catch(fallback);

  function build(svg){
    if(!window.gsap) return;
    var Q=function(s,c){return Array.prototype.slice.call((c||svg).querySelectorAll(s));};
    var N=function(e,a){return parseFloat(e.getAttribute(a))||0;};

    var lines=Q('path[stroke-linecap="round"][stroke-width="1.5"]');
    var dots=Q('rect[width="8"][height="8"][rx="4"]').map(function(r){return r.parentNode;});
    var months=Q('path[fill="#F8F8F8"][fill-opacity="0.7"]:not([opacity])');
    var cursor=svg.querySelector('path[fill="#282420"]');

    var bars=Q('rect[rx="10"]').map(function(r){
      var g=r.parentNode, st=g.querySelector('rect[rx="9.25"]'), cg=g.querySelector("g[clip-path]"), clip=null;
      if(cg){ var m=(cg.getAttribute("clip-path")||"").match(/#([^)]+)\)/); if(m){ var cp=svg.querySelector('clipPath[id="'+m[1]+'"]'); if(cp) clip=cp.querySelector("rect"); } }
      var texts=Q("path",g).filter(function(p){return p.getAttribute("fill")!=="#282420";});
      texts.forEach(function(x){ x.__o=x.getAttribute("opacity")!==null?parseFloat(x.getAttribute("opacity")):1; });
      return {g:g,fill:r,stroke:st,texts:texts,clip:clip,pf:g.querySelector('rect[rx="8"]'),ps:g.querySelector('rect[rx="7.25"]'),y:N(r,"y"),active:!!(st&&st.getAttribute("stroke")==="#F8F8F8")};
    }).sort(function(a,b){return a.y-b.y;});
    if(!bars.length) return;

    bars.forEach(function(b){ b.wF=N(b.fill,"width"); b.wS=b.stroke?N(b.stroke,"width"):0; b.wP=b.pf?N(b.pf,"width"):0; b.wPS=b.ps?N(b.ps,"width"):0; b.wC=b.clip?N(b.clip,"width"):0; });
    var hot=bars.filter(function(b){return b.active;})[0]||bars[0];

    gsap.set(svg,{opacity:0});
    gsap.set(lines,{scaleY:0,transformOrigin:"50% 0%"});
    gsap.set(dots,{scale:0,opacity:0,transformOrigin:"50% 50%"});
    gsap.set(months,{opacity:0,y:12});
    bars.forEach(function(b){
      gsap.set(b.g,{opacity:0,y:-10});
      gsap.set(b.fill,{attr:{width:46}});
      if(b.stroke) gsap.set(b.stroke,{attr:{width:44.5}});
      if(b.pf) gsap.set(b.pf,{attr:{width:30}});
      if(b.ps) gsap.set(b.ps,{attr:{width:28.5}});
      if(b.clip) gsap.set(b.clip,{attr:{width:30}});
      gsap.set(b.texts,{opacity:0});
    });
    if(cursor) gsap.set(cursor,{opacity:0,scale:.6,x:-(hot.wP-30),transformOrigin:"0% 0%"});

    var tl=gsap.timeline({paused:true,defaults:{ease:"power3.out"}});
    tl.to(svg,{opacity:1,duration:.5},0)
      .from(svg,{scale:.97,transformOrigin:"50% 50%",duration:1.2,ease:"power2.out"},0)
      .to(lines,{scaleY:1,duration:1,ease:"power4.out",stagger:{each:.03}},.1)
      .to(dots,{scale:1,opacity:1,duration:.7,ease:"back.out(3)",stagger:{each:.03}},.25)
      .to(months,{opacity:1,y:0,duration:.6,stagger:{each:.06}},.35);

    bars.forEach(function(b,i){
      var t=.55+i*.14;
      tl.to(b.g,{opacity:1,y:0,duration:.6,ease:"power2.out"},t)
        .to(b.fill,{attr:{width:b.wF},duration:1,ease:"power4.out"},t);
      if(b.stroke) tl.to(b.stroke,{attr:{width:b.wS},duration:1,ease:"power4.out"},t);
      if(b===hot) return;
      var p=t+.2;
      if(b.pf) tl.to(b.pf,{attr:{width:b.wP},duration:.8,ease:"power3.out"},p);
      if(b.ps) tl.to(b.ps,{attr:{width:b.wPS},duration:.8,ease:"power3.out"},p);
      if(b.clip) tl.to(b.clip,{attr:{width:b.wC},duration:.8,ease:"power3.out"},p);
      tl.to(b.texts,{opacity:function(i,t){return t.__o;},duration:.5},p+(b.clip?.3:.6));
    });

    var hero=.55+bars.length*.14+.3;
    if(cursor) tl.to(cursor,{opacity:1,scale:1,duration:.4,ease:"back.out(2)"},hero-.25);
    if(hot.pf) tl.to(hot.pf,{attr:{width:hot.wP},duration:1.1,ease:"power2.inOut"},hero);
    if(hot.ps) tl.to(hot.ps,{attr:{width:hot.wPS},duration:1.1,ease:"power2.inOut"},hero);
    if(hot.clip) tl.to(hot.clip,{attr:{width:hot.wC},duration:1.1,ease:"power2.inOut"},hero);
    if(cursor) tl.to(cursor,{x:0,duration:1.1,ease:"power2.inOut"},hero).to(cursor,{scale:.82,duration:.12,yoyo:true,repeat:1,ease:"power2.inOut"},hero+1.05);
    tl.to(hot.texts,{opacity:function(i,t){return t.__o;},duration:.5},hero+.3);

    tl.eventCallback("onComplete",function(){
      gsap.timeline({repeat:-1,repeatDelay:2.6})
        .to(lines,{attr:{"stroke-opacity":.4},duration:.3,stagger:{each:.025}})
        .to(lines,{attr:{"stroke-opacity":.1},duration:.55,stagger:{each:.025}},.2);
      if(cursor) gsap.to(cursor,{y:"+=3",duration:1.6,ease:"sine.inOut",yoyo:true,repeat:-1});
      if(hot.stroke) gsap.to(hot.stroke,{attr:{"stroke-opacity":.75},duration:1.4,ease:"sine.inOut",yoyo:true,repeat:-1});
      bars.forEach(function(b,i){ gsap.to(b.g,{y:i%2?-2.5:2.5,duration:2.4+i*.2,ease:"sine.inOut",yoyo:true,repeat:-1,delay:i*.15}); });
      var d=gsap.timeline({repeat:-1,repeatDelay:2.2,delay:1.2,defaults:{ease:"power2.inOut"}});
      if(hot.pf) d.to(hot.pf,{attr:{width:hot.wP+18},duration:.8},0);
      if(hot.ps) d.to(hot.ps,{attr:{width:hot.wPS+18},duration:.8},0);
      if(hot.clip) d.to(hot.clip,{attr:{width:hot.wC+18},duration:.8},0);
      if(cursor) d.to(cursor,{x:18,duration:.8},0);
      if(hot.pf) d.to(hot.pf,{attr:{width:hot.wP},duration:1},"+=.4");
      if(hot.ps) d.to(hot.ps,{attr:{width:hot.wPS},duration:1},"<");
      if(hot.clip) d.to(hot.clip,{attr:{width:hot.wC},duration:1},"<");
      if(cursor) d.to(cursor,{x:0,duration:1},"<");
    });

    if(window.matchMedia&&matchMedia("(prefers-reduced-motion: reduce)").matches){ tl.progress(1).pause(); return; }

    if("IntersectionObserver" in window){
      var io=new IntersectionObserver(function(es){ es.forEach(function(e){ if(e.isIntersecting){ tl.play(); io.disconnect(); } }); },{threshold:.25});
      io.observe(host);
    } else { tl.play(); }

    host.addEventListener("mousemove",function(e){
      var r=host.getBoundingClientRect();
      gsap.to(svg,{rotateY:((e.clientX-r.left)/r.width-.5)*6,rotateX:-((e.clientY-r.top)/r.height-.5)*6,transformPerspective:1200,transformOrigin:"50% 50%",duration:.6,ease:"power2.out"});
    });
    host.addEventListener("mouseleave",function(){ gsap.to(svg,{rotateX:0,rotateY:0,duration:.8,ease:"power3.out"}); });
  }
})();
</script>

About Us

Selling points timeline

Embed selling_point_code on the About Us page. As you scroll, .timeline_line_fill grows down the .timeline_line, and each .timeline_item switches on (marker, year and content) the moment the line reaches it. Scrolling back up reverses it.

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script>
var Webflow = Webflow || [];
Webflow.push(function () {
  // Check reduced motion preference
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

  var line = document.querySelector('.timeline_list .timeline_line');
  var fill = line ? line.querySelector('.timeline_line_fill') : null;
  var items = Array.from(document.querySelectorAll('.timeline_list .timeline_item'));

  if (!line || !fill || !items.length) return;

  var P = items.map(function (it) {
    return {
      el: it,
      dot: it.querySelector('.timeline_marker'),
      year: it.querySelector('p'),
      body: it.querySelector('.timeline_content'),
      on: false,
      y: 0
    };
  });

  // Initial GSAP Setups
  gsap.set(fill, { scaleY: 0, transformOrigin: '50% 0%' });
  P.forEach(function (p) {
    if (p.dot) gsap.set(p.dot, { scale: 0.5, opacity: 0.25 });
    if (p.year) gsap.set(p.year, { opacity: 0, x: -8 });
    if (p.body) gsap.set(p.body, { opacity: 0, y: 16 });
  });

  function measure() {
    var r = line.getBoundingClientRect();
    P.forEach(function (p) {
      if (!p.dot) {
        p.y = p.el.getBoundingClientRect().top - r.top;
        return;
      }
      var dr = p.dot.getBoundingClientRect();
      p.y = dr.top + dr.height / 2 - r.top;
    });
    return r;
  }

  function show(p, on) {
    if (p.dot) {
      gsap.to(p.dot, {
        scale: on ? 1 : 0.5,
        opacity: on ? 1 : 0.25,
        duration: on ? 0.5 : 0.3,
        ease: on ? 'back.out(2.2)' : 'power2.in',
        overwrite: 'auto'
      });
    }
    if (p.year) {
      gsap.to(p.year, {
        opacity: on ? 1 : 0,
        x: on ? 0 : -8,
        duration: on ? 0.5 : 0.3,
        ease: 'power2.out',
        delay: on ? 0.05 : 0,
        overwrite: 'auto'
      });
    }
    if (p.body) {
      gsap.to(p.body, {
        opacity: on ? 1 : 0,
        y: on ? 0 : 16,
        duration: on ? 0.6 : 0.3,
        ease: 'power2.out',
        delay: on ? 0.1 : 0,
        overwrite: 'auto'
      });
    }
  }

  var ticking = false, last = -1;
  function update() {
    var r = measure(), h = r.height || 1;
    var startY = window.innerHeight * 0.72;
    var p = Math.min(1, Math.max(0, (startY - r.top) / h));

    if (p !== last) {
      last = p;
      gsap.to(fill, { scaleY: p, duration: 0.45, ease: 'power3.out', overwrite: 'auto' });
    }

    var reached = p * h;
    P.forEach(function (it) { show(it, reached >= it.y - 1); });
  }

  function onScroll() {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(function () {
      ticking = false;
      update();
    });
  }

  window.addEventListener('scroll', onScroll, { passive: true });
  window.addEventListener('resize', onScroll);
  update();
});
</script>

Services

Stacked services scroll

Embed services_scroll on the Services page, for the section with class svc_scroll. On desktop the section pins and each scroll step slides the next service card up over the previous one; the service names on the left work as clickable navigation. Below 768px the cards stay in normal flow.

<script>
(function() {
  // Safe async script loader that satisfies Webflow code validators
  function loadGSAP(callback) {
    if (window.gsap) {
      callback(window.gsap);
      return;
    }
    if (window.Webflow && window.Webflow.require) {
      try {
        var wGsap = window.Webflow.require('gsap');
        if (wGsap) { callback(wGsap); return; }
      } catch(e) {}
    }
    var script = document.createElement('script');
    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js';
    script.onload = function() { callback(window.gsap); };
    document.head.appendChild(script);
  }

  function ready(fn) {
    if (document.readyState !== 'loading') {
      fn();
    } else {
      document.addEventListener('DOMContentLoaded', fn);
    }
  }

  ready(function() {
    var sec = document.querySelector('.svc_scroll');
    if (!sec) return;

    loadGSAP(function(gsapObj) {
      if (!gsapObj) return;

      var container = sec.querySelector('.container');
      var layout = sec.querySelector('.services_layout');
      var wrap = sec.querySelector('.service_wrap');
      var list = sec.querySelector('.service_list');

      var cards = Array.prototype.slice.call(sec.querySelectorAll('.service_list .service_collaction_item'));
      var names = Array.prototype.slice.call(sec.querySelectorAll('.list_num_muted'));
      var total = Math.min(cards.length, names.length) || cards.length;
      if (!total) return;

      var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var isDesktop = window.matchMedia('(min-width: 768px)');
      var DUR = reduce ? 0 : 0.8;
      var current = -1;

      sec.style.setProperty('--svc-n', total);

      // Desktop sticky container layout setup
      if (isDesktop.matches) {
        sec.style.setProperty('height', 'calc(var(--svc-n, 5) * 80svh)', 'important');
        sec.style.setProperty('padding-top', '0px', 'important');
        sec.style.setProperty('padding-bottom', '0px', 'important');

        if (container) {
          container.style.setProperty('position', 'sticky', 'important');
          container.style.setProperty('top', '0px', 'important');
          container.style.setProperty('height', '100svh', 'important');
          container.style.setProperty('display', 'flex', 'important');
          container.style.setProperty('align-items', 'center', 'important');
        }

        if (layout) {
          layout.style.setProperty('width', '100%', 'important');
          layout.style.setProperty('height', 'auto', 'important');
          layout.style.setProperty('justify-content', 'center', 'important');
          layout.style.setProperty('align-items', 'center', 'important');
        }

        if (wrap) {
          wrap.style.setProperty('flex', '1 1 0%', 'important');
          wrap.style.setProperty('width', '100%', 'important');
          wrap.style.setProperty('max-width', '624px', 'important');
        }

        if (list) {
          list.style.setProperty('max-height', 'none', 'important');
          list.style.setProperty('height', 'clamp(360px, calc(100vh - 280px), 620px)', 'important');
          list.style.setProperty('border-radius', '20px', 'important');
          list.style.setProperty('overflow', 'hidden', 'important');
          list.style.setProperty('position', 'relative', 'important');
          list.style.setProperty('isolation', 'isolate', 'important');
        }
      }

      function getParts(card) {
        return {
          img: card.querySelector('.cover_image.service'),
          blur: card.querySelector('.service_image_blur'),
          icon: card.querySelector('.services_icon_bg'),
          glass: card.querySelector('.about_glass'),
          head: card.querySelector('.services_glass_head'),
          bars: Array.prototype.slice.call(card.querySelectorAll('.services_chart .home_bar')),
          days: Array.prototype.slice.call(card.querySelectorAll('.services_days .glass_day'))
        };
      }

      var cardParts = cards.map(getParts);
      var mainTL = null, chartTL = null;

      // Card initial styling and stacking position
      cards.forEach(function(card, i) {
        var active = (i === 0);
        var parts = cardParts[i];

        card.style.setProperty('position', 'absolute', 'important');
        card.style.setProperty('inset', '0px', 'important');
        card.style.setProperty('width', '100%', 'important');
        card.style.setProperty('height', '100%', 'important');
        card.style.setProperty('border-radius', '20px', 'important');
        card.style.setProperty('overflow', 'hidden', 'important');

        gsapObj.set(card, {
          yPercent: active ? 0 : 102,
          scale: 1,
          filter: 'brightness(1)',
          zIndex: i + 1,
          willChange: 'transform'
        });

        if (parts.img) {
          parts.img.style.setProperty('height', '100%', 'important');
          parts.img.style.setProperty('width', '100%', 'important');
          parts.img.style.setProperty('object-fit', 'cover', 'important');
          gsapObj.set(parts.img, { scale: active ? 1 : 1.08 });
        }
        if (parts.icon) {
          parts.icon.style.setProperty('border-radius', '24px', 'important');
          gsapObj.set(parts.icon, { opacity: active ? 1 : 0, y: active ? 0 : 40 });
        }
        if (parts.glass) gsapObj.set(parts.glass, { opacity: active ? 1 : 0, y: active ? 0 : 18 });
        parts.bars.forEach(function(b) { gsapObj.set(b, { transformOrigin: '50% 100%', scaleY: 1 }); });
      });

      // Side nav items click and hover bindings
      names.forEach(function(nameEl, i) {
        nameEl.setAttribute('role', 'button');
        nameEl.setAttribute('tabindex', '0');
        nameEl.style.setProperty('cursor', 'pointer', 'important');
        gsapObj.set(nameEl, { opacity: 0.35, filter: 'blur(4px)' });

        nameEl.addEventListener('click', function() { setActive(i); });
        nameEl.addEventListener('pointerenter', function() {
          if (i !== current) gsapObj.to(nameEl, { opacity: 0.6, filter: 'blur(2px)', duration: 0.3, overwrite: 'auto' });
        });
        nameEl.addEventListener('pointerleave', function() {
          if (i !== current) gsapObj.to(nameEl, { opacity: 0.35, filter: 'blur(4px)', duration: 0.3, overwrite: 'auto' });
        });
      });

      function playInnerAnimations(index) {
        var parts = cardParts[index];
        if (mainTL && mainTL.kill) mainTL.kill();
        if (chartTL && chartTL.kill) chartTL.kill();

        if (reduce) {
          if (parts.glass) gsapObj.set(parts.glass, { opacity: 1, y: 0 });
          if (parts.head) gsapObj.set(parts.head, { opacity: 1, x: 0 });
          parts.bars.forEach(function(b) { gsapObj.set(b, { scaleY: 1 }); });
          parts.days.forEach(function(d) { gsapObj.set(d, { opacity: 1, y: 0 }); });
          return;
        }

        mainTL = gsapObj.timeline({ defaults: { ease: 'power2.out' }, delay: 0.1 });

        if (parts.glass) mainTL.fromTo(parts.glass, { opacity: 0, y: 18 }, { opacity: 1, y: 0, duration: 0.5 }, 0);
        if (parts.head) mainTL.fromTo(parts.head, { opacity: 0, x: -10 }, { opacity: 1, x: 0, duration: 0.4 }, 0.1);
        if (parts.bars.length) mainTL.fromTo(parts.bars, { scaleY: 0.05 }, { scaleY: 1, duration: 0.7, stagger: 0.05, ease: 'power3.out' }, 0.12);
        if (parts.days.length) mainTL.fromTo(parts.days, { opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.35, stagger: 0.04 }, 0.2);

        if (parts.bars.length) {
          chartTL = gsapObj.timeline({ repeat: -1, yoyo: true, delay: 1 });
          parts.bars.forEach(function(b, k) {
            chartTL.to(b, { scaleY: [0.85, 0.95, 0.82, 0.98, 0.9][k % 5], duration: 2.5, ease: 'sine.inOut' }, k * 0.08);
          });
        }
      }

      function setActive(index) {
        if (index === current) return;
        current = index;

        cards.forEach(function(card, j) {
          var parts = cardParts[j];
          var status = (j === index) ? 'on' : ((j < index) ? 'past' : 'next');

          gsapObj.to(card, {
            yPercent: status === 'on' ? 0 : (status === 'past' ? -8 : 102),
            scale: status === 'past' ? 0.94 : 1,
            filter: status === 'past' ? 'brightness(0.45)' : 'brightness(1)',
            duration: DUR,
            ease: 'power3.inOut',
            overwrite: 'auto'
          });

          if (parts.img) gsapObj.to(parts.img, { scale: status === 'on' ? 1 : 1.08, duration: DUR * 1.2, ease: 'power2.out', overwrite: 'auto' });
          if (parts.icon) gsapObj.to(parts.icon, { opacity: status === 'on' ? 1 : 0, y: status === 'on' ? 0 : 40, duration: DUR, delay: status === 'on' ? 0.2 : 0, ease: 'power2.out', overwrite: 'auto' });
          if (parts.blur) gsapObj.to(parts.blur, { opacity: status === 'on' ? 1 : 0.4, duration: DUR, ease: 'power2.out', overwrite: 'auto' });
          if (status !== 'on' && parts.glass) gsapObj.to(parts.glass, { opacity: 0, y: 18, duration: 0.3, ease: 'power2.in', overwrite: 'auto' });
        });

        playInnerAnimations(index);

        names.forEach(function(nameEl, j) {
          var active = (j === index);
          nameEl.classList.toggle('is-active', active);
          gsapObj.to(nameEl, {
            opacity: active ? 1 : 0.35,
            filter: active ? 'blur(0px)' : 'blur(4px)',
            color: active ? '#ffffff' : '',
            duration: 0.4,
            overwrite: 'auto'
          });
        });
      }

      // Scroll position tracking & trigger calculation
      var ticking = false;
      function checkScroll() {
        if (ticking) return;
        ticking = true;
        requestAnimationFrame(function() {
          ticking = false;
          if (!isDesktop.matches) return;
          var rect = sec.getBoundingClientRect();
          var scrollLength = Math.max(1, sec.offsetHeight - window.innerHeight);
          var progress = Math.min(0.9999, Math.max(0, -rect.top / scrollLength));
          var nextIndex = Math.floor(progress * total);
          setActive(nextIndex);
        });
      }

      window.addEventListener('scroll', checkScroll, { passive: true });
      window.addEventListener('resize', checkScroll);
      
      // Initial trigger execution
      setActive(0);
      checkScroll();
    });
  });
})();
</script>

Service detail

Results chart

Embed in the chart card on the Services Template (service detail) page. The bars (.chart-bar) drop in with an elastic ease and the labels fade in; hovering a bar stretches it and clicking highlights it. Only the script is shown here; the SVG chart markup stays in the embed.

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>

<script>
  document.addEventListener("DOMContentLoaded", () => {
    const bars = document.querySelectorAll(".chart-bar");
    const labels = document.querySelectorAll(".chart-label");

    // Initial state: bars collapsed at the top line
    gsap.set(bars, { scaleY: 0 });
    gsap.set(labels, { opacity: 0, y: -10 });

    // Main Entry Timeline
    const tl = gsap.timeline();

    // Card Fade-In
    tl.from("#chart-svg", {
      duration: 0.8,
      opacity: 0,
      scale: 0.95,
      ease: "power2.out"
    })
    // Top-to-Bottom drop/grow effect using scaleY: 1
    .to(bars, {
      scaleY: 1,
      duration: 1.2,
      stagger: 0.12,
      ease: "elastic.out(1, 0.55)"
    }, "-=0.3")
    // Labels Fade In
    .to(labels, {
      opacity: 0.72,
      y: 0,
      duration: 0.5,
      stagger: 0.1,
      ease: "power2.out"
    }, "-=1.0");

    // Interactive Hover and Click Handlers
    bars.forEach((bar) => {
      bar.addEventListener("mouseenter", () => {
        gsap.to(bar, { scaleY: 1.06, duration: 0.3, ease: "power2.out" });
      });

      bar.addEventListener("mouseleave", () => {
        gsap.to(bar, { scaleY: 1, duration: 0.3, ease: "power2.out" });
      });

      bar.addEventListener("click", () => {
        bars.forEach((b) => {
          b.classList.remove("active");
          gsap.to(b, { fill: "#F8F8F8", fillOpacity: 0.1, duration: 0.3 });
        });

        bar.classList.add("active");
        gsap.to(bar, { fill: "#DBDBD2", fillOpacity: 1, duration: 0.3 });

        // Elastic bounce effect from top
        gsap.fromTo(bar,
          { scaleY: 0.85 },
          { scaleY: 1, duration: 0.6, ease: "elastic.out(1.2, 0.4)" }
        );
      });
    });
  });
</script>
Ready to turn your next challenge into an opportunity?
Start your Strategy Call
Start your Strategy Call