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
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
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
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
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
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
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
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
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
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
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
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
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>