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-front
Div Block: Class Name:
logo-back
2. 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>