Solvex brand logo with 'sol' in gray and 'vex' in white text.

Instructions

Find easy to follow instructions
GSAP Guide
Animation Code
All GSAP animations used in this template are collected here. On this page, you’ll find guidance on how to locate and edit them. Each code block comes with extra notes to make it easier to understand.
Custom Code
Lenis Scroll
Smooth scrolling effect for the template using Lenis with GSAP integration for precise scroll updates and animations.
<link rel="stylesheet" href="https://unpkg.com/lenis@1.3.15/dist/lenis.css" />
<script src="https://unpkg.com/lenis@1.3.15/dist/lenis.min.js"></script>

<script>
  const lenis = new Lenis({ duration: 1.4 });

  lenis.on('scroll', ScrollTrigger.update);
  gsap.ticker.add((time) => {
    lenis.raf(time * 1000);
  });
  gsap.ticker.lagSmoothing(0);
</script>
Text Scramble Hover
Custom text scramble effect using GSAP, dynamically cycling through characters before smoothly revealing the original text across navigation, buttons, portfolio, and blog interactions.
// =========================================================
    // 1. TEXT SCRAMBLE ON HOVER
    // =========================================================
    function scrambleText(el, finalText, options = {}) {
      const chars = options.chars || '01<>[]{}#$@';
      const duration = options.duration || 0.5;
      const revealPad = options.revealDelay ?? 0.15;
      const length = finalText.length;
      const state = { progress: 0 };

      return gsap.to(state, {
        progress: 1,
        duration: duration,
        ease: 'none',
        onUpdate: () => {
          const revealCount = Math.floor(state.progress * (1 + revealPad) * length);
          let output = '';
          for (let i = 0; i < length; i++) {
            if (finalText[i] === ' ') {
              output += ' ';
            } else if (i < revealCount) {
              output += finalText[i];
            } else {
              output += chars[Math.floor(Math.random() * chars.length)];
            }
          }
          el.textContent = output;
        },
        onComplete: () => {
          el.textContent = finalText;
          if (options.onComplete) options.onComplete();
        },
      });
    }

    const menus = document.querySelectorAll('.menu-navbar, .button, .link-navbar, .item-work, .card-blog');

    menus.forEach((menu) => {
      const text = menu.querySelector('.scramble');
      if (!text) return;

      const originalText = text.textContent.trim();
      const originalColor = getComputedStyle(text).color;
      let active = false;

      menu.addEventListener('mouseenter', () => {
        if (active) return;
        active = true;

        gsap.to(text, { color: '#22d3ee', duration: 0.2 });

        scrambleText(text, originalText, {
          chars: '01<>[]{}#$@',
          duration: 0.5,
          revealDelay: 0.15,
          onComplete: () => {
            gsap.to(text, {
              color: originalColor,
              duration: 0.4,
            });
            active = false;
          },
        });
      });

      menu.addEventListener('mouseleave', () => {
        // re-scramble halus saat mouse keluar, tanpa mengunci state "active"
        scrambleText(text, originalText, {
          chars: '01<>[]{}#$@',
          duration: 0.5,
          revealDelay: 0,
        });
      });
    });
Scroll Linked Ripple Animation
Dynamic ripple bar animation driven by GSAP ScrollTrigger, creating a wave-like scaling and opacity effect that responds smoothly to the user's scroll progress across the service section.
// =========================================================
    // 2. RIPPLE BAR (scroll-linked)
    // =========================================================
    const wrapper = document.querySelector('.wrapper-ripple');

    if (wrapper) {
      const template = wrapper.querySelector('.ripple-bar');

      if (template) {
        const gap = 2;
        const influence = 8;

        function buildRipple() {
          wrapper.querySelectorAll('.ripple-bar:not(:first-child)').forEach((el) => el.remove());

          const wrapperHeight = wrapper.offsetHeight;
          const barHeight = template.offsetHeight;
          const total = Math.floor(wrapperHeight / (barHeight + gap));

          wrapper.style.display = 'flex';
          wrapper.style.flexDirection = 'column';
          wrapper.style.alignItems = 'center';
          wrapper.style.gap = `${gap}px`;

          for (let i = 1; i < total; i++) {
            const clone = template.cloneNode(true);
            clone.classList.add('ripple-clone');
            wrapper.appendChild(clone);
          }

          updateRipple(0);
        }

        function updateRipple(progress) {
          const bars = wrapper.querySelectorAll('.ripple-bar');
          const active = progress * (bars.length - 1);

          bars.forEach((bar, index) => {
            const distance = Math.abs(index - active);
            const strength = Math.exp(-(distance * distance) / (2 * influence * influence));

            gsap.set(bar, {
              scaleX: 0.2 + strength * 0.8,
              opacity: 0.2 + strength * 0.8,
              transformOrigin: 'center center',
            });
          });
        }

        buildRipple();
        window.addEventListener('resize', buildRipple);

        ScrollTrigger.create({
          trigger: '.track-service',
          start: 'center-=15% bottom',
          end: 'bottom-=10% top',
          scrub: true,
          onUpdate: (self) => updateRipple(self.progress),
        });
      }
    }
Scroll Responsive Navbar
Dynamic navbar behavior using GSAP, automatically hiding the navigation and logo when scrolling down and smoothly revealing them again when scrolling up for a cleaner browsing experience.
// =========================================================
    // 3. NAVBAR HIDE ON SCROLL
    // =========================================================
    if (window.innerWidth >= 992) {
      const navbar = document.querySelector('.navbar');
      const logo = document.querySelector('.link-logo-nav');

      if (navbar && logo) {
        let lastScroll = window.pageYOffset;
        let isHidden = false;

        window.addEventListener('scroll', () => {
          const currentScroll = window.pageYOffset;

          if (currentScroll > lastScroll && currentScroll > 50) {
            if (!isHidden) {
              isHidden = true;

              gsap.to(logo, { yPercent: -100, opacity: 0, duration: 0.6, ease: 'power3.inOut' });
              gsap.to(navbar, {
                xPercent: 100,
                opacity: 0,
                duration: 0.6,
                ease: 'power3.inOut',
                onComplete: () => {
                  gsap.set([navbar, logo], { display: 'none' });
                },
              });
            }
          } else if (currentScroll < lastScroll) {
            if (isHidden) {
              isHidden = false;

              gsap.set([navbar, logo], { display: '' });
              gsap.set(navbar, { xPercent: 100, opacity: 0 });
              gsap.set(logo, { yPercent: -100, opacity: 0 });

              gsap.to(navbar, { xPercent: 0, opacity: 1, duration: 0.6, ease: 'power3.out' });
              gsap.to(logo, { yPercent: 0, opacity: 1, duration: 0.6, ease: 'power3.out' });
            }
          }

          lastScroll = currentScroll;
        });
      }
    }
Glitch Text Reveal Animation
A dynamic text reveal effect powered by GSAP SplitText and ScrollTrigger, creating a futuristic glitch transition with smooth color shifts as each word enters the viewport.
<script>
  document.addEventListener('DOMContentLoaded', function () {
    gsap.registerPlugin(ScrollTrigger, SplitText);

    var CONFIG = {
      fromColor: 'rgba(255, 255, 255, 0.15)',
      glitchColor: '#22d3ee',
      toColor: 'rgba(255, 255, 255, 1)',

      flickerCount: 4,
      flickerSpeed: 0.045,
      settleDuration: 0.3,
      settleEase: 'power2.out',

      splitType: 'words',
      stagger: 0.06,

      start: 'top 85%',
      end: 'bottom 60%',
      toggleActions: 'play none play reverse',
    };

    function buildGlitchTimeline(target) {
      var tl = gsap.timeline();

      for (var i = 0; i < CONFIG.flickerCount; i++) {
        var flickerToGlitch = i % 2 === 0;
        tl.set(
          target,
          {
            color: flickerToGlitch ? CONFIG.glitchColor : CONFIG.fromColor,
          },
          i === 0 ? 0 : '+=' + CONFIG.flickerSpeed,
        );
      }

      tl.to(
        target,
        {
          color: CONFIG.toColor,
          duration: CONFIG.settleDuration,
          ease: CONFIG.settleEase,
        },
        '+=' + CONFIG.flickerSpeed,
      );

      return tl;
    }

    var elements = document.querySelectorAll('.reveal-animation');

    elements.forEach(function (el) {
      gsap.set(el, { visibility: 'hidden' });

      var split = new SplitText(el, {
        type: CONFIG.splitType,
        wordsClass: 'reveal-word',
        linesClass: 'reveal-line',
      });

      var targets = split.words && split.words.length ? split.words : split.lines;

      gsap.set(targets, {
        display: 'inline-block',
        willChange: 'color',
        color: CONFIG.fromColor,
      });

      gsap.set(el, { visibility: 'visible' });

      var master = gsap.timeline({
        scrollTrigger: {
          trigger: el,
          start: CONFIG.start,
          end: CONFIG.end,
          toggleActions: CONFIG.toggleActions,
        },
      });

      targets.forEach(function (word, i) {
        master.add(buildGlitchTimeline(word), i * CONFIG.stagger);
      });
    });

    window.addEventListener('load', function () {
      ScrollTrigger.refresh();
    });
  });
</script>