Jump to content
Search Community

GSAP responsive issues in Nextjs (fullscreen animation), unit conversion error

Ganbatte test
Moderator Tag

Recommended Posts

 

 

Hello! I'm new to the world of GSAP, although I have experience with CSS. I'm using NextJS 14 in my project. I've watched quite a few videos from GSAP, but I haven't found any discussing clearing inline styles with scrub: true. I have an image positioned in the center of the screen using position: absolute, and I've used GSAP animations to make it go fullscreen on scroll. My problem is that when I resize the screen, it simply enlarges to the original size instead of adjusting to the current size. I've tried various units to specify the width for GSAP, such as using percentages (100%), viewport width (100vw), or window.innerWidth, but none seem to work properly for achieving fullscreen. I don't understand this behavior because it seems to convert the inline style to pixels even when I specify percentages. I've seen in multiple videos that the GSAP team emphasizes not worrying about units, but I've only encountered problems. For instance, I set the image to have a width of 200px in CSS and specified 100% width as the target value in GSAP. Even before the scroller reaches the trigger start, it applies some inline style causing the image to widen. However, if I use window.innerWidth, there's no change in size. But the main issue still remains with responsiveness. If I give GSAP 50%, why doesn't it render that in the inline style? Why does it convert it to pixels? This makes it completely unresponsive, and I have no idea how to make it adjust to fullscreen when the size changes. Is there a way to clear the GSAP inline style when a screen size is changed (I don't mean breakpoints, I know about gsap.matchMedia(), but it is not a solution for this) and recalculate it? I'd be incredibly grateful for any help. Thanks in advance!

Here is my css:

.img-container {
    position: relative;
}
 
.hero-cont {
    position: relative;
    display: flex;
    flex-direction: column;
    min-height: 100svh;
    overflow-x: hidden !important;
}
.hero__content {
    width: 100%;
    margin-top: 28vh;
    .hero__title {
        font-size: clamp(3rem, 6vw + 0.1rem, 5rem);
        font-weight: 600;
        line-height: 1.1;
        letter-spacing: 1px;
        margin-bottom: 30px;
    }
    .hero__text {
        font-size: clamp(1rem, 6vw + 0.1rem, 1.5rem);
        max-width: 35ch;
    }
}
.hero__bg {
    position: absolute;
    z-index: -1;
    object-fit: cover;
}
 
.hero-img__container {
    position: absolute;
    height: 60%;
    width: 200px;
    // left: 25%;
    position: absolute;
    left: 0;
    right: 0;
    top: 25%;
    margin-inline: auto;
    rotate: 15deg;
    // transform: translate(-30%, -50%);
    z-index: -1;
    border: 1px solid white;
    border-width: 15px 15px 40px 15px;
    box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.2);
    // transition: all 400ms ease-in;
    .hero-photo__img {
        object-fit: cover;
        width: 100%;
        height: 100%;
    }
}

Here is my component:
 

'use client';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import Hero from '@/components/Hero';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import Image from 'next/image';
 
const heroImg = '/assets/images/hero/hero2.webp';
const heroPhotoImg = '/assets/images/hero/hero-photo.webp';
 
export default function Home() {
    const heroComp = useRef(null);
 
    useLayoutEffect(() => {
        gsap.registerPlugin(ScrollTrigger);
        let ctx = gsap.context(() => {
            let tl = gsap.timeline({
                scrollTrigger: {
                    trigger: heroComp.current, //trigger
                    start: '+=10 top',
                    end: 'bottom top',
                    toggleActions: 'play',
                    scrub: true,
                    markers: true,
                    pin: true,
                },
            });
            tl.to('.hero-img__container', {
                left: 0,
                top: 0,
                borderWidth: 0,
                transform: 'rotate(0)',
                height: '100%',
                width: window.innerWidth,
            });
            tl.to('.hero__content', {
                opacity: 0,
            });
        }, heroComp);
        return () => {
            ctx.revert();
        };
    }, []);
 
    return (
        <>
            {/* <Hero /> */}
            <section ref={heroComp} className='hero-cont cont full-size'>
                <Image src={heroImg} alt='hero' className='hero__bg' fill />
                <div className='hero__content'>
                    <h1 className='hero__title'>
                        Beautiful Moment <br /> is Everything
                    </h1>
                    <p className='hero__text'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Aut, tempora?</p>
                </div>
                <div className='hero-img__container'>
                    <Image fill src={heroPhotoImg} alt='hero' className='hero-photo__img' />
                </div>
            </section>
        </>
    );
}
Link to comment
Share on other sites

It's pretty tough to troubleshoot without a minimal demo - the issue could be caused by CSS, markup, a third party library, your browser, an external script that's totally unrelated to GSAP, etc. Would you please provide a very simple CodePen or Stackblitz that demonstrates the issue? 

 

Please don't include your whole project. Just some colored <div> elements and the GSAP code is best. See if you can recreate the issue with as few dependancies as possible. If not, incrementally add code bit by bit until it breaks. Usually people solve their own issues during this process! If not, then at least we have a reduced test case which greatly increases your chances of getting a relevant answer.

 

Here's a starter CodePen that loads all the plugins. Just click "fork" at the bottom right and make your minimal demo

See the Pen aYYOdN by GreenSock (@GreenSock) on CodePen

 

Using a framework/library like React, Vue, Next, etc.? 

CodePen isn't always ideal for these tools, so here are some Stackblitz starter templates that you can fork and import the gsap-trial NPM package for using any of the bonus plugins: 

 

Please share the StackBlitz link directly to the file in question (where you've put the GSAP code) so we don't need to hunt through all the files. 

 

Once we see an isolated demo, we'll do our best to jump in and help with your GSAP-specific questions. 

Link to comment
Share on other sites

Here is the showcase of my problem, on a clean new project: https://gsaptest2.netlify.app/

Please try to resize the window, after loaded the images.
style.scss

*,
*::before,
*::after {
    margin: 0;
    padding: 0;
    box-sizing: border-box !important;
    text-decoration: none;
    list-style: none;
    // outline: 1px solid rgb(255, 0, 0);
}
 
.img-container {
    position: relative;
}
 
.hero-cont {
    position: relative;
    display: flex;
    flex-direction: column;
    min-height: 100svh;
}
.hero__content {
    width: 100%;
    margin-top: 28vh;
    .hero__title {
        font-size: clamp(3rem, 6vw + 0.1rem, 5rem);
        font-weight: 600;
        line-height: 1.1;
        letter-spacing: 1px;
        margin-bottom: 30px;
    }
    .hero__text {
        font-size: clamp(1rem, 6vw + 0.1rem, 1.5rem);
        max-width: 35ch;
    }
}
.hero__bg {
    position: absolute;
    z-index: -1;
    object-fit: cover;
}
 
.hero-img__container {
    position: absolute;
    height: 60%;
    width: 400px;
    // left: 25%;
    position: absolute;
    left: 0;
    right: 0;
    top: 25%;
    margin-inline: auto;
    rotate: 15deg;
    // transform: translate(-30%, -50%);
    z-index: -1;
    border: 1px solid white;
    border-width: 15px 15px 40px 15px;
    box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.2);
    // transition: all 400ms ease-in;
    .hero-photo__img {
        object-fit: cover;
        width: 100%;
        height: 100%;
    }
}

page.tsx:

'use client';
import { useLayoutEffect, useRef } from 'react';
import gsap from 'gsap';
import Image from 'next/image';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
 
const heroImg = '/assets/images/hero/hero2.webp';
const heroPhotoImg = '/assets/images/hero/hero-photo.webp';
 
export default function Home() {
    const heroComp = useRef(null);
 
    useLayoutEffect(() => {
        gsap.registerPlugin(ScrollTrigger);
        let ctx = gsap.context(() => {
            let tl = gsap.timeline({
                scrollTrigger: {
                    trigger: heroComp.current, //trigger
                    start: '+=10 top',
                    end: 'bottom top',
                    toggleActions: 'play',
                    scrub: true,
                    markers: true,
                    pin: true,
                },
            });
            tl.to('.hero-img__container', {
                left: 0,
                top: 0,
                borderWidth: 0,
                transform: 'rotate(0)',
                height: '100%',
                width: window.innerWidth,
            });
            tl.to('.hero__content', {
                opacity: 0,
            });
        }, heroComp);
        return () => {
            ctx.revert();
        };
    }, []);
    return (
        <main >
            <section ref={heroComp} className='hero-cont cont full-size'>
                <Image src={heroImg} alt='hero' className='hero__bg' fill />
                <div className='hero__content'>
                    <h1 className='hero__title'>
                        Beautiful Moment <br /> is Everything
                    </h1>
                    <p className='hero__text'>Lorem ipsum dolor sit amet consectetur adipisicing elit. Aut, tempora?</p>
                </div>
                <div className='hero-img__container'>
                    <Image fill src={heroPhotoImg} alt='hero' className='hero-photo__img' />
                </div>
            </section>
        </main>
    );
}
Link to comment
Share on other sites

Hi @Ganbatte welcome to the forum! 

 

We can't debug a 'live' website, there is just no way to modify the code. That is why we ask for a minimal demo (please read our forum guidelines) in a Codepen or if your issue is specific to a framework (it usually isn't) you could use one of our many Stackblitz starter templates!

 

If you could provide that, we'll be happy to take a look for you!

Link to comment
Share on other sites

Hi,

 

I'm not 100% I follow what exactly you're trying to do. Here is my best guess:

See the Pen rNPopvM by GreenSock (@GreenSock) on CodePen

 

Hopefully this helps. If you're not looking for this, please be super specific as to what you're trying to achieve (include a live example that does what you're aiming for) so we can get a clear idea.

Happy Tweening!

Link to comment
Share on other sites

Hello Rodrigo!
First of all, thank you for your help, but unfortunately, that's not what I had in mind. I've created the editable version which you can find at my link. My problem is that when I resize the window, the image doesn't adjust accordingly. Also, I don't understand why I can't specify CSS values like percentages or viewport width (vw) through GSAP. It seems to convert them directly into pixels, which I find puzzling.
I'm mean for this: https://stackblitz.com/edit/nextjs-auvnxa?file=pages%2Findex.js

 

Link to comment
Share on other sites

While testing on StackBlitz, I managed to figure out a few things. The reason it wasn't responsive was that I used window.innerWidth. I did this because GSAP was overriding the size set in CSS upon loading, which I'm still unsure why it's doing. These lines caused the issue, although theoretically, they shouldn't affect it. When I commented out GSAP, there were no problems. For instance, I set a width of 20% in CSS, but GSAP changed it to 25% even before the animation. Why this happens is unclear.

Additionally, it slightly alters the value even without this code. For instance, I set it to 20%, but GSAP overrides it inline to 19.9858%. I'm perplexed as to why it does this.

Here's the code (all of this is available on the StackBlitz interface):

@mixin containerChild() {
  > div:not(.img-container) {
    max-width: 1200px;
    margin-inline: auto;
    padding-inline: 10px;
  }
}
.cont {
  @include containerChild();
  min-height: 100svh;
}

difference.png

Link to comment
Share on other sites

4 hours ago, Ganbatte said:

My problem is that when I resize the window, the image doesn't adjust accordingly.

Sorry, I'm still having issues getting this through my thick skull 😞. What I'm seeing in that demo is exactly what should happen. Also the values being interpolated by GSAP in the image container are in percentage for the width and height as far as I can see. Maybe I'm missing something here. How the image should adjust?

 

I would recommend you to use scale values (if possible) for the image in order to get better performance. Animating Height, Width, Top/Left/Ritgh/Bottom, margins, paddings, etc.  can be very expensive because it'll trigger a repaint on every value change, so is better to use scale. That's why in my demo I set the image to be in it's final value (covering the entire container) and then I scale it down in order to animate it to scale 1.

 

As for the values being a bit odd, sometimes complex calculations can result in a decimal position, very far away, to get an unexpected value. Is that presenting a problem in your setup currently?

 

Happy Tweening!

Link to comment
Share on other sites

Thanks for the tip and the information, Rodrigo! Unfortunately, I can't use scale or valut in this case because I also modify the cropping of the image. However, I'm satisfied with the solution. But I have a problem: when I put it in a separate component and create the timeline in the main component, then pass it over, I can't properly use the scroll trigger. How can I use scroll triggers for multiple animations on one page? What am I doing wrong? The scroll trigger only work with my first animations.
I really want to get the hang of using GSAP, it seems like a great tool, but I just haven't quite gotten the hang of it yet. I would be extremely grateful for any help.
Thank you so much in advance.
here is the demo:https://stackblitz.com/edit/nextjs-wr4ktj?file=pages%2Findex.js,pages%2FHero.js,pages%2F_app.js

Link to comment
Share on other sites

Hi,

 

I only see one issue in your code, you are creating a to() instance with a ScrollTrigger config inside of a timeline. That's a logic problem as explained here:

https://gsap.com/resources/st-mistakes#nesting-scrolltriggers-inside-multiple-timeline-tweens

 

On that note, I don't really see the need to create a reference to that particular timeline in the parent component TBH. The animation is controlled by ScrollTrigger and has scrub in it. Nothing in the parent component should have any control over that timeline, since is controlled by the scroll position using ScrollTrigger.

 

My advice would be to create the timeline for the Hero component inside that component and move the ScrollTrigger configuration to the config object of the timeline.

 

// Hero.js
export default function Hero() {
  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      gsap.registerPlugin(ScrollTrigger);
      const timeline = gsap.timeline({
        scrollTrigger: {
          trigger: container.current,
          start: '+=10 top',
          end: 'bottom top',
          toggleActions: 'play',
          markers: true,
          scrub: true,
          pin: true,
        },
      });
      timeline.to('.hero-img__container', {
        left: 0,
        top: 0,
        borderWidth: 0,
        transform: 'rotate(0)',
        height: '100%',
        width: '100%',
        duration: 3,
      });
      timeline.to('.hero__content', {
        opacity: 0,
        transform: 'translateY(50px)',
      });
    }, container);
    return () => ctx.revert();
  }, []);
  
  return (
    // JSX Here
  );
};

 

Hopefully this helps.

Happy Tweening!

Link to comment
Share on other sites

Thanks again, Rodrigo. I've tried this solution myself, but it results in having 2 scroll triggers on the homepage. If I create more sections on the homepage that I want to animate, there could potentially be 5-10 scroll triggers. Isn't the goal of the scroll trigger to have just one on the homepage in certain cases, and then modify the trigger itself along with the pinning element? Is this possible, or am I thinking about it in a completely wrong way? How do larger websites usually implement this? Unfortunately, I can't find any tutorials online that cover this scenario, they all demonstrate a single case. I've improved the demo and added an extra section to make my point clearer. Thank you very much in advance for your help!
link is the same: https://stackblitz.com/edit/nextjs-hvbhri?file=pages%2Findex.js,pages%2FHero.js,pages%2FBeforeAfterScroll.js,styles%2Fglobals.scss

Link to comment
Share on other sites

I'm on my phone now so I can't take a look at the demo.

 

But you can have as many scrolltrigger instances as you want as long as you create them in the order they apper in the screen. Just create them inside each component and put those components in the right order.

 

This page of a Next project was made with ScrollTrigger a few years ago using that approach, and as you can see nothing is breaking

 

https://wiredave.com/artificial-intelligence

 

Happy Tweening!

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...