Instructions
Webflow Template User Guide
<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {
function initFloatingIcon(containerSelector, iconSelector) {
document.querySelectorAll(containerSelector).forEach((container) => {
const icon = container.querySelector(iconSelector);
if (!icon) return;
icon.style.pointerEvents = "none";
let targetX = 0;
let targetY = 0;
let currentX = 0;
let currentY = 0;
let active = false;
gsap.set(icon, {
xPercent: -50,
yPercent: -50,
x: 0,
y: 0,
opacity: 0,
scale: 0.7
});
container.addEventListener("mouseenter", (e) => {
active = true;
const rect = container.getBoundingClientRect();
targetX = currentX = e.clientX - rect.left;
targetY = currentY = e.clientY - rect.top;
gsap.set(icon, {
x: currentX,
y: currentY
});
gsap.to(icon, {
opacity: 1,
scale: 1,
duration: 0.35,
ease: "power3.out",
overwrite: true
});
});
container.addEventListener("mousemove", (e) => {
const rect = container.getBoundingClientRect();
targetX = e.clientX - rect.left;
targetY = e.clientY - rect.top;
});
container.addEventListener("mouseleave", () => {
active = false;
gsap.to(icon, {
opacity: 0,
scale: 0.7,
duration: 0.25,
ease: "power2.out",
overwrite: true
});
});
gsap.ticker.add(() => {
if (!active) return;
currentX += (targetX - currentX) * 0.18;
currentY += (targetY - currentY) * 0.18;
gsap.set(icon, {
x: currentX,
y: currentY
});
});
});
}
initFloatingIcon(".project-image", ".icon-project-wrap");
initFloatingIcon(".blog-image-wrap", ".icon-blog-wrap");
});
</script><script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {
// ==================================================
// LENIS SMOOTH SCROLL (DESKTOP)
// ==================================================
// ── Guard against double-init ──────────────────────────
if (window._lenisScrollInit) return;
window._lenisScrollInit = true;
// desktop only
const isDesktop = window.matchMedia("(min-width: 992px)").matches;
if (!isDesktop) return;
const lenisScript = document.createElement("script");
lenisScript.src = "https://unpkg.com/lenis@1.3.21/dist/lenis.min.js";
lenisScript.onload = function () {
if (typeof Lenis === "undefined") {
console.error("[LenisScroll] Lenis failed to load.");
return;
}
const lenis = new Lenis({
lerp: 0.07, // the smaller, the smoother
smoothWheel: true,
wheelMultiplier: 0.8,
autoRaf: false,
// Force document-level scroll measurement. Without this, an
// overflow:hidden element earlier in the DOM (e.g. a sticky
// section wrapper) can cause Lenis to under-measure total
// scrollHeight, cutting off content at the end of the page
// (footer) even though native scroll renders it fine.
wrapper: window,
content: document.documentElement
});
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
// so it can be called from other scripts
window.lenis = lenis;
if (typeof ResizeObserver !== "undefined") {
const resizeObserver = new ResizeObserver(function () {
lenis.resize();
});
resizeObserver.observe(document.body);
} else {
// Fallback for browsers without ResizeObserver support
setTimeout(function () {
lenis.resize();
}, 500);
}
// ── Fix: keep scroll limit correct on viewport resize ──
window.addEventListener("resize", function () {
lenis.resize();
});
};
document.body.appendChild(lenisScript);
});
</script>%, +, $, or text labels). It works with any typography class by targeting the first child element inside each counter container..text-xl, .text-l, .text-xxl) — displays the animated number<script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {
gsap.registerPlugin(ScrollTrigger);
document.querySelectorAll(".counter-number-wrap").forEach((counter) => {
// Get the first child element (.text-xl, .text-l, .text-xxl, etc.)
const number = counter.firstElementChild;
if (!number) return;
const text = number.textContent.trim();
// Extract numeric value (supports decimals)
const numeric = text.match(/[\d.]+/);
if (!numeric) return;
const finalValue = parseFloat(numeric[0]);
// Count decimal places
const decimals = (numeric[0].split(".")[1] || "").length;
// Preserve prefix/suffix
const prefix = text.substring(0, text.indexOf(numeric[0]));
const suffix = text.substring(text.indexOf(numeric[0]) + numeric[0].length);
const obj = { value: 0 };
gsap.to(obj, {
value: finalValue,
duration: 2,
ease: "expo.out",
scrollTrigger: {
trigger: counter,
start: "top 85%",
once: true
},
onUpdate() {
number.textContent =
prefix +
obj.value.toFixed(decimals) +
suffix;
}
});
});
});
</script><script>
window.Webflow = window.Webflow || [];
window.Webflow.push(function () {
// ==================================================
// CARD SWIPE (MOBILE TOUCH SLIDER)
// ==================================================
// ── Guard against double-init ──────────────────────────
if (window._cardSwipeInit) return;
window._cardSwipeInit = true;
// ── GSAP availability check ────────────────────────────
if (typeof gsap === "undefined") {
console.error("[CardSwipe] GSAP not found.");
return;
}
// ── Config ──────────────────────────────────────────────
const MOBILE_MIN_WIDTH = 360;
const MOBILE_MAX_WIDTH = 479;
const SWIPE_DISTANCE_RATIO = 0.2;
const SWIPE_VELOCITY_THRESHOLD = 0.5;
const DIRECTION_LOCK_THRESHOLD = 8;
// ── DOM check ───────────────────────────────────────────
const viewport = document.querySelector('.projects-list-wrap');
const cards = viewport
? Array.from(viewport.children).filter(function (el) {
return el.classList.contains('collection-list-wrapper-projects');
})
: [];
if (!viewport || cards.length === 0) {
console.warn('[CardSwipe] .projects-list-wrap or .collection-list-wrapper-projects cards not found.');
return;
}
// .swipe-indicator-wrap lives OUTSIDE .projects-list-wrap.
const activeIndicator = viewport.parentElement
? viewport.parentElement.querySelector('.swipe-indicator-wrap .swipe-indicator-line')
: null;
if (!activeIndicator) {
console.warn('[CardSwipe] .swipe-indicator-line not found outside .projects-list-wrap scope — swipe will still work, indicator will not update.');
}
const totalCards = cards.length;
// ── State ───────────────────────────────────────────────
let track = null;
let cardWidth = 0;
let gapPx = 0;
let currentIndex = 0;
let baseX = 0;
let isSwipeActive = false;
let isDragging = false;
let directionLock = null;
let startX = 0;
let startY = 0;
let startTime = 0;
// ── Helpers ─────────────────────────────────────────────
function checkMobileRange() {
const w = window.innerWidth;
return w >= MOBILE_MIN_WIDTH && w <= MOBILE_MAX_WIDTH;
}
function buildTrack() {
track = document.createElement('div');
track.style.display = 'flex';
track.style.flexFlow = 'row';
track.style.willChange = 'transform';
viewport.style.position = viewport.style.position || 'relative';
viewport.style.overflow = 'hidden';
cards.forEach(function (card) {
track.appendChild(card);
});
viewport.appendChild(track);
}
function teardownTrack() {
if (!track) return;
cards.forEach(function (card) {
card.style.width = '';
card.style.flex = '';
viewport.appendChild(card);
});
track.remove();
track = null;
}
function measure() {
cardWidth = viewport.getBoundingClientRect().width;
gapPx = parseFloat(window.getComputedStyle(viewport).columnGap) || 0;
cards.forEach(function (card) {
card.style.width = cardWidth + 'px';
card.style.flex = '0 0 auto';
});
track.style.columnGap = gapPx + 'px';
}
function updateIndicator(index) {
if (!activeIndicator) return;
const percent = ((index + 1) / totalCards) * 100;
gsap.to(activeIndicator, {
width: percent + '%',
duration: 0.3,
ease: 'power2.out'
});
}
function goToIndex(index, animate) {
currentIndex = Math.max(0, Math.min(totalCards - 1, index));
const targetX = -currentIndex * (cardWidth + gapPx);
if (animate === false) {
gsap.set(track, { x: targetX });
} else {
gsap.to(track, {
x: targetX,
duration: 0.4,
ease: 'power3.out'
});
}
baseX = targetX;
updateIndicator(currentIndex);
}
// ── Touch handlers ──────────────────────────────────────
function onTouchStart(e) {
isDragging = true;
directionLock = null;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
startTime = Date.now();
gsap.killTweensOf(track);
}
function onTouchMove(e) {
if (!isDragging) return;
const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY;
const deltaX = currentX - startX;
const deltaY = currentY - startY;
if (directionLock === null) {
if (Math.abs(deltaX) > DIRECTION_LOCK_THRESHOLD || Math.abs(deltaY) > DIRECTION_LOCK_THRESHOLD) {
directionLock = Math.abs(deltaX) > Math.abs(deltaY) ? 'x' : 'y';
}
}
if (directionLock === 'y') {
isDragging = false;
return;
}
if (directionLock === 'x') {
e.preventDefault();
gsap.set(track, { x: baseX + deltaX });
}
}
function onTouchEnd(e) {
if (!isDragging || directionLock !== 'x') {
isDragging = false;
directionLock = null;
return;
}
isDragging = false;
const endX = (e.changedTouches && e.changedTouches[0].clientX) || startX;
const deltaX = endX - startX;
const elapsed = Math.max(Date.now() - startTime, 1);
const velocity = Math.abs(deltaX) / elapsed;
const passedDistance = Math.abs(deltaX) > cardWidth * SWIPE_DISTANCE_RATIO;
const passedVelocity = velocity > SWIPE_VELOCITY_THRESHOLD;
let nextIndex = currentIndex;
if (passedDistance || passedVelocity) {
nextIndex = deltaX < 0 ? currentIndex + 1 : currentIndex - 1;
}
goToIndex(nextIndex);
directionLock = null;
}
// ── Enable / disable swipe mode ─────────────────────────
function enableSwipe() {
if (isSwipeActive) return;
isSwipeActive = true;
buildTrack();
measure();
goToIndex(0, false);
track.addEventListener('touchstart', onTouchStart, { passive: true });
track.addEventListener('touchmove', onTouchMove, { passive: false });
track.addEventListener('touchend', onTouchEnd);
track.addEventListener('touchcancel', onTouchEnd);
}
function disableSwipe() {
if (!isSwipeActive) return;
isSwipeActive = false;
if (track) {
track.removeEventListener('touchstart', onTouchStart);
track.removeEventListener('touchmove', onTouchMove);
track.removeEventListener('touchend', onTouchEnd);
track.removeEventListener('touchcancel', onTouchEnd);
}
teardownTrack();
currentIndex = 0;
}
function handleResize() {
const nowMobile = checkMobileRange();
if (nowMobile && !isSwipeActive) {
enableSwipe();
} else if (!nowMobile && isSwipeActive) {
disableSwipe();
} else if (nowMobile && isSwipeActive) {
measure();
goToIndex(currentIndex, false);
}
}
// ── Init ────────────────────────────────────────────────
if (checkMobileRange()) {
enableSwipe();
}
window.addEventListener('resize', handleResize);
});
</script>