برچسب: Basics

  • Mastering Carousels with GSAP: From Basics to Advanced Animation

    Mastering Carousels with GSAP: From Basics to Advanced Animation


    Carousels are a fairly common UI pattern (there are many excellent carousel and slider examples available on Codrops). While carousel designs vary depending on the use case, the following demos explore how the GreenSock Animation Platform (GSAP) can be used to achieve seamless looping, smooth animations, and ultimately, a better user experience.

    This article is for frontend designers and developers interested in enhancing the functionality and visual appeal of a standard horizontal carousel. Familiarity with JavaScript and basic GSAP methods will be helpful, but anyone looking for inspiration and practical examples may find the following content useful.

    What You’ll Learn

    • Basic carousel implementation using HTML and CSS
    • How to use gsap.utils.wrap() and horizontalLoop()
    • Advanced animation techniques, including image parallax and function-based values

    Our Basic Carousel

    Let’s start with a horizontally scrolling carousel using only HTML and CSS:

    <div class="carousel">
        
        <div class="carousel-slide">
          <img src="https://images.unsplash.com/photo-1659733582156-d2a11801e59f?q=50&w=1600">
          <h2>We're No</h2>
          <h5>Strangers to love</h5>
        </div>
            
        ...
    
    </div>
    .carousel {
      width: 100vw;
      height: 80vh;
      gap: 10px;
      overflow-x: auto;
      scroll-snap-type: x mandatory;
      display: flex;
      -webkit-overflow-scrolling: touch;
    }
    
    .carousel-slide {
      position: relative;
      flex: 0 0 50%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      color: white;
      scroll-snap-align: center;
      overflow: hidden;
    }
    
    .carousel-slide img {
      position: absolute;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
    
    h2 {
      position: relative;
      margin: 0;
      font-size: 1.8rem;
    }
    
    h5 {
      position: relative;
      margin: 2% 0 0 0;
      font-size: 1rem;
      font-weight: 100;
      letter-spacing: 0.3px;
    }
    
    /* Simplify the scroll bar appearance */
    ::-webkit-scrollbar {
      height: 13px;
    }
    
    ::-webkit-scrollbar-track {
      background: transparent;
    }
    
    ::-webkit-scrollbar-thumb {
      border-top: 6px solid #000;
      background: #555;
      width: 50%;
    }
    
    ::-webkit-scrollbar-thumb:hover {
      background: #bbb;
    }
    
    @media (max-width: 500px) {
      .carousel-slide {
        flex: 0 0 80%;
      }
    
      ::-webkit-scrollbar-thumb {
        width: 80%;
      }
    }

    Here’s the result:

    It uses scroll snapping and some custom styling on the scrollbar. Nothing fancy, but it works even when JavaScript is disabled.

    Note that the HTML above is intentionally concise. However, in production, it’s important to follow accessibility best practices, including using alt text on images and descriptive ARIA attributes for screen reader users.

    Building on the Foundation – GSAP Demo 1A

    To see how GSAP can enhance a carousel, we’ll explore two different approaches—the first using gsap.utils.wrap(). Wrap is one of several handy utility methods included in gsap.js—no plugin required! Given a min/max range, it returns a value within that range:

     gsap.utils.wrap(5, 10, 12); // min 5, max 10, value to wrap 12: returns 7

    The example above returns 7 because 12 is 2 more than the maximum of 10, so it wraps around to the start and moves 2 steps forward from there. In a carousel, this can be used to loop infinitely through the slides.

    Here’s a simple demo of how it can be applied:

    In the HTML, a <nav> block has been added that contains previous/next buttons and progress text:

    <nav class="carousel-nav">
      <button class="prev" tabindex="0" aria-label="Previous Slide"></button>
      <button class="next" tabindex="0" aria-label="Next Slide"></button>
      <div>1/8</div>
    </nav>

    A few new rules have been added to the CSS, most importantly to .carousel-slide-abs:

    .carousel-slide-abs {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      width: 75vw;
      height: 70vh;
    }

    In the JS, we override the carousel’s scroll-snap-type and display the <nav> block. Since we no longer have a scrollable area, the buttons are necessary to maintain keyboard accessibility. Safari requires tabindex="0" to allow users to tab to them. Additionally, aria-labels are important since the buttons have no visible text content.

    We apply the new class to each slide, which effectively stacks them all in the center. We also set the initial opacity: 1 for the first slide and 0 for the rest:

    gsap.set(".carousel", { "scroll-snap-type": "none" });
    
    gsap.set(".carousel-nav", { display: "block" });
    
    slides.forEach((slide, i) => {
      slide.classList.add("carousel-slide-abs");
      gsap.set(slide, { opacity: (i === 0 ? 1 : 0) });
    });

    Next, we need a function that transitions to the previous or next slide. changeSlide() is passed a direction parameter of either positive or negative 1. Inside this function, we:

    1. Fade out the current slide
    2. Update the current slide index using gsap.utils.wrap()
    3. Fade in the new current slide
    4. Update the progress text

    The different easing on the outro and intro tweens helps prevent excessive overlapping opacity during the crossfade.

    next.addEventListener("click", () => changeSlide( 1 ));
    prev.addEventListener("click", () => changeSlide( -1 ));
    
    function changeSlide( dir ) {
      
      gsap.to(slides[currentIndex], { opacity: 0, ease: "power3" });
      
      currentIndex = gsap.utils.wrap(0, slides.length, (currentIndex += dir));
      
      gsap.to(slides[currentIndex], { opacity: 1, ease: "power3.inOut" });
      
      gsap.set(".carousel-nav div", { innerText: `${currentIndex + 1}/${slides.length}` });
    
    }

    Polishing the Transition – GSAP Demo 1B

    To take this idea further, let’s add more detail to the outro and intro animations:

    For the 3D perspective to work, we’ve added perspective: 750px to .carousel-slide-abs in the CSS.

    Instead of targeting the slides themselves, we set the opacity of their child elements to 0—except for those in the first slide.

     gsap.set(slide.children, { opacity: (i === 0 ? 1 : 0) });

    Then, we do the following inside changeSlide():

    1. Store a reference to the outgoing slide’s children
    2. Update currentIndex, just as before
    3. Create a const for the incoming slide’s children
    4. Kill tweens on both slides’ children to prevent conflicts if slides change rapidly
    5. Create a timeline for the transition:
    gsap.timeline({ defaults:{ ease: "expo" } })
      // update progress text
      .set(".carousel-nav div", { innerText: `${currentIndex + 1}/${slides.length}` })
    
      // old slide outro
      .to(oldLayers[0], {
        duration: 0.3,
        rotateY: (dir<0 ? -75 : 75),
        scale: 0.6,
        ease: "power2.in"
      }, 0)
      .to(oldLayers, {
        duration: 0.3,
        opacity: 0,
        ease: "power2.in"
      }, 0)
    
      // new slide intro
      .to(newLayers, {
        opacity: 1,
        ease: "power1.inOut",
        stagger: 0.2
      }, 0.2)
      .fromTo(newLayers[0], {
        rotateY: (dir<0 ? 90 : -90),
        scale: 0.6
      },{
        rotateY: 0,
        scale: 1
      }, 0.3)
      .fromTo([newLayers[1], newLayers[2]], {
        y: 35
      },{
        duration: 1,
        y: 0,
        stagger: 0.14
      }, 0.4);

    Easing and staggers help smooth out and space the movement. The dir parameter modifies the rotationY, adding a subtly unique motion to previous and next actions.

    This basic setup can be easily customized further. Animating a clip-path, applying a blur filter, or experimenting with additional 3D transforms could all produce interesting results.

    A Different Approach – GSAP Demo 2A

    Another way to create a seamless looping carousel with GSAP is to use the horizontalLoop() helper function. Although GSAP helper functions aren’t officially part of the core library, they’re a handy collection of code snippets and shortcuts. They also serve as great learning resources for writing more advanced GSAP code.

    This specific helper function animates elements along their x-axis and repositions them once they’re out of view to create an infinite loop. Here’s a basic implementation:

    Again, we override the CSS and display the <nav> element. Then we call horizontalLoop(), which takes two parameters: an array of the carousel slides and a config object for setting various options.

    const loop = horizontalLoop(slides, {
      paused: true,       // no auto-scroll
      paddingRight: 10,   // match the 10px flex gap
      center: true,       // snap the active slide to the center
      onChange: (slide, index) => { // called when the active slide changes
        if (activeSlide) {
          gsap.to(".active", { opacity: 0.3 });
          activeSlide.classList.remove("active");
        }
        slide.classList.add("active");
        activeSlide = slide;
        gsap.to(".active", { opacity: 1, ease: "power2.inOut" });
        gsap.set(".carousel-nav div", { innerText: `${index + 1}/${slides.length}` });
      }
    });

    The most notable of these options is the onChange callback, where we can write code that executes each time the active slide changes. In this example, we’re removing and adding the “active” class name and tweening the opacity to draw more focus to the center slide.

    The helper function returns a timeline with several useful added methods, including next(), previous(), and toIndex(). We’ll use these to add navigation functionality to our previous/next buttons, as well as to the individual slides:

    next.addEventListener("click", () => loop.next({ duration: 1, ease: "expo" }));
    prev.addEventListener("click", () => loop.previous({ duration: 1, ease: "expo" }));
    
    // each slide can function as a button to activate itself
    slides.forEach((slide, i) => {
      slide.addEventListener("click", () => loop.toIndex(i, {duration: 1, ease: "expo"}))
    });

    Finally, we set the initial carousel state by adjusting the opacity of each slide and calling toIndex() with no tween duration, which centers the active slide.

    gsap.set(".carousel-slide", { opacity: (i) => (i === 0 ? 1 : 0.3) });
    
    loop.toIndex(0, { duration: 0 }); 

    If you’re unfamiliar with function-based values in GSAP, this is an amazing feature—definitely check out that link to learn how they work. Here, we’re iterating through each element with the class name “carousel-slide,” returning an opacity value of 1 for the first slide and 0.3 for the rest.

    The remainder of the JS is just the helper function, copied and pasted from the GSAP docs demo. In most cases, you won’t need to modify anything inside it. (We’ll look at an exception in Demo 2C.)

    Add Draggable & InertiaPlugin – GSAP Demo 2B

    To make the carousel move on drag, we’ll need two plugins: Draggable and the Inertia Plugin. Once those scripts are included, you can set draggable: true in the config object.

    In addition to drag behavior, this iteration includes some text animation, with logic to prevent it from running on the first load (plus hover in/out animations on the nav buttons).

    onChange: (slide, index) => { // called when the active slide changes
      if (activeSlide) {
        gsap.to(".carousel h2, .carousel h5", { overwrite: true, opacity: 0, ease: "power3" });
        gsap.to(".active", { opacity: 0.3 });
        activeSlide.classList.remove("active");
      }
      slide.classList.add("active");
      activeSlide = slide;
      
      // intro animation for new active slide
      gsap.timeline({ defaults:{ ease:"power1.inOut" } })
    
        // fade in the new active slide
        .to(".active", { opacity: 1, ease: "power2.inOut" }, 0)
    
        // fade out the progress text, change its value, fade it back in
        .to(".carousel-nav div", { duration: 0.2, opacity: 0, ease: "power1.in" }, 0)
        .set(".carousel-nav div", { innerText: `${index + 1}/${slides.length}` }, 0.2)
        .to(".carousel-nav div", { duration: 0.4, opacity: 0.5, ease: "power1.inOut" }, 0.2)
    
        // fade in the text elements and translate them vertically
        .to(".active h2, .active h5", { opacity: 1, ease: "power1.inOut" }, 0.3)
        .fromTo(".active h2, .active h5", { y:(i)=>[40,60][i] },{ duration: 1.5, y: 0, ease: "expo" }, 0.3)
    
        // skip active slide animation on first run
        .progress( firstRun? 1: 0 )
    }

    Adding Parallax – GSAP Demo 2C

    To make the movement more engaging, let’s calculate each slide’s horizontal progress and use it to create a parallax effect.

    Until now, we haven’t modified the helper function. However, to calculate slide progress, this version includes one change inside horizontalLoop().

    Now, every time the carousel timeline updates, slideImgUpdate() is called. This function sets each image’s xPercent based on the progress of its parent slide. Progress is 0 when the slide is offstage to the left, and 1 when it’s offstage to the right.

    function slideImgUpdate(){
      slides.forEach( slide => {
        const rect = slide.getBoundingClientRect();
        const prog = gsap.utils.mapRange(-rect.width, innerWidth, 0, 1, rect.x);
        const val = gsap.utils.clamp(0, 1, prog );
        gsap.set(slide.querySelector("img"), {
          xPercent: gsap.utils.interpolate(0, -50, val)
        });
      });
    }

    GSAP utility functions mapRange(), interpolate(), and clamp() make the progress calculation much easier. Note, in the CSS, the width of .carousel-slide img is increased to 150%, so there will be enough image for a 50% horizontal movement.

    Taking It Further

    There are endless ways you could build on these demos, customizing both appearance and functionality. A few ideas include:

    • Modify how many slides are shown at once—a single, full-frame version could be interesting, as could several smaller slides to create a cover flow effect. In both of those examples, the progress indicator also became a fun area for experimentation.
    • Additional details could be added by calling custom functions inside the helper function’s onPress, onRelease, or onThrowComplete callbacks. Here’s one more iteration on Demo 2, where the entire carousel shrinks while the pointer is held down.
    • The carousel could even serve as navigation for a separate animated page element, like on Nite Riot.
    • If you want the carousel to respond to mouse wheel movements, GSAP’s Observer plugin offers an easy way to handle those events.
    • With GSAP’s matchMedia(), you can specify different animations for various viewport widths and tailor behavior for users who prefer reduced motion.



    Source link