Instructions.
Webflow Template User Guide
Implementation Guide: Scroll Reset Behavior (Page Load) + Logo Flip Animation
Follow the steps below to implement smooth cursor interaction and dynamic logo flip animation in Webflow.
1. Webflow Structure (HTML)
A. Logo Flip Animation
Create this structure:
Div Block: Class Name:
logo-flip
(container that rotates in 3D)
Div Block: Class Name:
logo-frontDiv Block: Class Name:
logo-back2. Style Settings (CSS)
Apply these specific styles to ensure the slider functions correctly:
.logo-flip
:
Position:
Relative
Custom properties:
transform-style : preserve-3d
Children
.logo-front
:
Position:
Absolute
Width:
100%
Height:
100%
Children
.logo-back
:
Position:
Absolute
Width:
100%
Height:
100%
2D & 3D Transforms:
rotate Y (180deg)
3. Adding the Custom Code
Apply these specific styles to ensure the Scroll Reset Behavior (Page Load) + Logo Flip Animation correctly:
Go to your Page Settings.
Scroll down to the Before </body> tag section.
Paste the following script:
A. Scroll Reset Behavior (Page Load)
<script>
setTimeout(() => {
try {
window.scrollTo(0, 0);
} catch (e) {}
}, 250); // Adjust timing if needed for smoother behavior
</script>B. Logo Flip Animation
<script>
window.Webflow ||= [];
window.Webflow.push(() => {
const flips = document.querySelectorAll(".logo-flip");
flips.forEach((el) => {
let flipped = false;
let isAnimating = false;
// Ensure proper 3D rendering
gsap.set(el, {
transformPerspective: 1000,
transformOrigin: "center"
});
function loop() {
const delay = 1500 + Math.random() * 3000;
setTimeout(() => {
// Prevent overlapping animations
if (isAnimating) {
loop();
return;
}
isAnimating = true;
flipped = !flipped;
gsap.to(el, {
rotateY: flipped ? 180 : 0,
duration: 0.9,
ease: "power2.inOut",
force3D: true,
onComplete: () => {
isAnimating = false;
}
});
loop();
}, delay);
}
loop();
});
});
</script>
Implementation Guide: Draggable Card (Mobile Only)
Follow the steps below to implement draggable card (mobile only) animation in Webflow.
Webflow Implementation Steps
1. Build the Structure
Create a main Div Block with the class
process-card-content
2. Add Cards
Inside the wrapper, add multiple Div Blocks with the class
card-process
3. Add Card Elements
Inside each card, include elements with these exact classes:
images-process
process-number
description-process
4. Configure Layout
Set
card-process
to
flex-shrink: 0
and provide a defined width (e.g.,
80vw
) to ensure proper spacing and snapping.
5. Insert Script
Paste the provided code into the Before
</body>
tag section of your Page Settings.
<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/Draggable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
// ─── Config ───────────────────────────────────────────────────────────────
const MOBILE_BP = 479; // synced with CSS @media max-width: 479px
const SWIPE_MIN = 30;
const RESIZE_DELAY = 150;
// ─── DOM References ───────────────────────────────────────────────────────
const wrapper = document.querySelector(".process-card-content");
if (!wrapper) return;
// Store original card order BEFORE touching the DOM at all
// This is important so disableDraggable can restore the original structure
const originalCards = Array.from(
wrapper.querySelectorAll(".card-process")
);
if (!originalCards.length) return;
const total = originalCards.length;
// ─── State ───────────────────────────────────────────────────────────────
let currentIndex = 0;
let draggableInstance = null;
let isActive = false;
let track = null; // track element is created/removed dynamically
let dragStartX = 0;
let resizeTimer = null;
const hasPlayed = new Set();
// ─── Register GSAP plugins (once only) ───────────────────────────────────
gsap.registerPlugin(Draggable, ScrollTrigger);
// ─── Helper: hide card content ────────────────────────────────────────────
function hideCardContent(card) {
const els = card.querySelectorAll(
".images-process, .process-number, .description-process"
);
gsap.set(els, { opacity: 0, y: 24, immediateRender: true });
}
// ─── Helper: play card entrance animation ────────────────────────────────
function playCardEntrance(card) {
const els = Array.from(card.querySelectorAll(
".images-process, .process-number, .description-process"
));
if (!els.length) return;
gsap.fromTo(
els,
{ opacity: 0, y: 24 },
{ opacity: 1, y: 0, duration: 0.35, ease: "power3.out", stagger: 0.1, delay: 0.1 }
);
}
// ─── Helper: calculate snap position per card ────────────────────────────
function getSnapPositions() {
const gap = 24;
return originalCards.map((c, i) => {
let offset = 0;
for (let j = 0; j < i; j++) offset += originalCards[j].offsetWidth + gap;
return -offset;
});
}
// ─── Navigate to a specific card index ───────────────────────────────────
function goTo(index) {
currentIndex = Math.max(0, Math.min(index, total - 1));
const snapX = getSnapPositions()[currentIndex];
gsap.to(track, {
x : snapX,
duration : 0.5,
ease : "power3.out",
onComplete() {
if (draggableInstance) draggableInstance.update();
if (!hasPlayed.has(currentIndex)) {
hasPlayed.add(currentIndex);
playCardEntrance(originalCards[currentIndex]);
}
},
});
originalCards.forEach((c, i) => {
gsap.to(c, {
scale : i === currentIndex ? 1 : 0.95,
opacity : i === currentIndex ? 1 : 0.5,
duration: 0.25,
ease : "power2.out",
});
});
}
// ─── Enable mobile mode ───────────────────────────────────────────────────
// KEY FIX: track div is created HERE (not outside),
// so the tablet DOM is never touched at all.
function enableDraggable() {
if (isActive) return;
isActive = true;
// Create track div and move cards into it
track = document.createElement("div");
track.className = "process-card-track";
track.style.cssText = "display:flex;gap:1.5rem;will-change:transform;";
originalCards.forEach(c => track.appendChild(c));
wrapper.appendChild(track);
gsap.set(wrapper, {
overflowX : "hidden",
cursor : "grab",
userSelect: "none",
});
// Hide all card content
originalCards.forEach(card => hideCardContent(card));
// ScrollTrigger for the first card
ScrollTrigger.create({
trigger: originalCards[0],
start : "top 80%",
once : true,
onEnter() {
if (!hasPlayed.has(0)) {
hasPlayed.add(0);
playCardEntrance(originalCards[0]);
}
},
});
// Attach Draggable to track
draggableInstance = Draggable.create(track, {
type : "x",
edgeResistance: 0.9,
onPress() {
dragStartX = this.x;
wrapper.style.cursor = "grabbing";
gsap.killTweensOf(track);
},
onRelease() {
wrapper.style.cursor = "grab";
const delta = this.x - dragStartX;
if (Math.abs(delta) < SWIPE_MIN) { goTo(currentIndex); return; }
goTo(delta < 0 ? currentIndex + 1 : currentIndex - 1);
},
})[0];
goTo(0);
ScrollTrigger.refresh();
}
// ─── Disable mobile mode, restore DOM to original state ──────────────────
// KEY FIX: cards are moved BACK to wrapper as direct children,
// then the track div is removed — so CSS grid on tablet/desktop works normally.
function disableDraggable() {
if (!isActive) return;
isActive = false;
// Kill ScrollTrigger & Draggable
ScrollTrigger.getAll().forEach(t => t.kill());
if (draggableInstance) {
draggableInstance.kill();
draggableInstance = null;
}
// Return cards to wrapper (as direct children) in original order
originalCards.forEach(card => {
// Clear all GSAP overrides on the card
gsap.set(card, { clearProps: "scale,opacity,x,y" });
// Clear overrides on inner card content
const els = card.querySelectorAll(
".images-process, .process-number, .description-process"
);
gsap.set(els, { clearProps: "opacity,y" });
wrapper.appendChild(card);
});
// Remove track div from the DOM entirely
if (track && track.parentNode) {
track.parentNode.removeChild(track);
}
track = null;
// Clear inline style overrides on wrapper
gsap.set(wrapper, { clearProps: "overflowX,cursor,userSelect" });
// Reset internal state
hasPlayed.clear();
currentIndex = 0;
}
// ─── Resize handler with state comparison ────────────────────────────────
function handleResize() {
const isMobile = window.innerWidth <= MOBILE_BP;
if (isMobile && !isActive) {
enableDraggable();
} else if (!isMobile && isActive) {
disableDraggable();
}
}
// ─── Init & resize listener ───────────────────────────────────────────────
handleResize();
window.addEventListener("resize", function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(handleResize, RESIZE_DELAY);
});
});
</script>