Jump to content
Search Community

ScrollTrigger behaving differently if target is a container instead of the body

iDad5 test
Moderator Tag

Go to solution Solved by Cassie,

Recommended Posts

Ok, this is a (kind of) follow-up to the last topic that @GreenSock saved my life with.

 

Background:

As with this kind of page, the annoying behavior of most mobile browsers to hide the address bar when the body scrolls and thereby triggering (unpredictable) resize events. Which I in turn have to ignore, but that leaves me with ugly leftover space. I can fill this for the price of recalculating everything, but then I have to watch out for when the address bar returns…

 

I could work with that, but as the behavior is inconsistent on devices, it seems not worth the effort. (Side quest:  does anyone know about a way to detect if the address bar overlays the top or the bottom part of a page?)  

 

To avoid body scroll at all, I try to pack my page into a container “window”—and I get unexpected results.

 

Core Topic

As you can see in the two CodePens when I do my stuff with the body as scroll target The pin spacer wraps around the container and the container stays in the pin-spacer while scrolling. Therefore, scrolling stops at the expected moment.

 

As you can see in the second pen, if I do (at least in my simple mind) the exact same thing one level down, something different happens:

The pin-spacer still wraps around the container but now when I scroll the pin-spacer is scrolled like before, but also the container is translated out of the pin-spacer by the same amount. Thereby doubling scroll speed (with which I could live) but also effectively doubling the height of the window's content, allowing to scroll the wannabe pined content out of the window.  

 

I guess I'm simply missing something obvious and will be pointed in the right direction easily. I did reduce my code from last time a lot, but I do know that it still might be deemed a bit more complex than absolutely necessary—I apologize if that's the case. And in case the issue is against my expectations hard to understand and exotic to love, I will try to boil it down some more.

 

The top one is the try with the window, the bottom one is the original version in the body 

 

See the Pen zYLeYev by iDad5 (@iDad5) on CodePen

 

See the Pen KKBJKrE by iDad5 (@iDad5) on CodePen

Link to comment
Share on other sites

I'm not quite sure what was going awry but at least there's a working version now. That's half the battle - now you can play spot the difference. That's a pretty foolproof way to figure out the issue.

I mainly added pinType: "transform" , anticipatePin and simplified the markup and CSS.

 

You had a body tag and a stray closing html tag in yours too, codepen also adds a body tag and an html tag, so maybe that was causing an issue?

  • Like 2
Link to comment
Share on other sites

Thank you. 

The body tag was not the problem (mine was simply removed, as was the leftover closing HTML tag). I had the exact same behavior in my local test environment.

 

 

My construction of the ScrollTrigger is somewhat unusual, I guess, and maybe I should have done it another way from the beginning—though I'm uncertain if I could have. 

The thing is that it works just fine with the body as the scroller. And completely restructuring and rewriting the whole thing would be hard, and I hope unnecessary. 

 

I try to describe how it works as I understand it by now:

 

I give the container a height of n-times that of the screen/panel (n being the number of panels) as there's nothing to scroll with the panels all positioned absolutely on top. 

 

ScrollTrigger wraps the container in the pin-spacer of the same height.

And sets the container to positioned 'fixed'.

The pin-spacer is the only element that actually scrolls, as all its contents are fixed.

ScrollTrigger moves along, scrubbing the timeline.

 

Works fine on the body

 

With another element than the body as a scroller ScrollTrigger moves the pined element exactly the same distance down with transform translate, that the pin-spacer is scrolled up.

As my container is n-times higher than it's content, it scrolls out of view, or to be more precise, it scrolls its empty bottom into view. As it is not fixed this time.

When trying to use fixed either in with ScrollTrigger's pin type or by setting the containers style to position fixed, there is noting to scroll, for the browser it seems, even though the pin-spacer is created and should be scrollable.

My guess is that by fixing the container, it 'covers' everything. So, pin type 'fixed' on another element than the body will likely only work if the pinned element is not full-size.

I will make some tests with pinSpacing true and a numeric value as the end point for ScrollTrigger, but I'm not sure if that will break other tings in my project, and my gut tells me that position fixed should perform better than translate.

I might have to deal with the mobile issues in another, likely more complex, way.

 

Therefore, I repeat my side quest question with more urgency: 

 

Does anyone know about a way to detect if the mobile address bar overlays the top or the bottom part of a page?

Link to comment
Share on other sites

6 hours ago, iDad5 said:

Does anyone know about a way to detect if the mobile address bar overlays the top or the bottom part of a page?

 No, there's not a way I'm aware of to find this out and a cursory google search doesn't return anything promising, sorry

 

6 hours ago, iDad5 said:

The thing is that it works just fine with the body as the scroller.

Unsurprising. This is the ideal way to approach ScrollTriggered animations. 

To quote from that thread.

Quote

what you're describing sounds like a consequence of the fact that modern browsers handle scrolling on a totally different thread than the main JS one, thus they're not synchronized. So you scroll, the browser moves the contents of the scroller (in the scrolling thread), renders that, and then a few milliseconds later it executes the JS thread and ScrollTrigger applies its transform adjustment and puts things where they're supposed to be. So the content renders at one position and then almost instantly it gets corrected. There's absolutely nothing ScrollTrigger can do (at least that I've found) to force those to stay synchronized. It's totally annoying. That's why ScrollTrigger uses pinType: "fixed" for things in the main scroller (body/documentElement), and "transform" for everything else. Browsers typically only use a different thread for the main/root element scrolling. And you can't use position: fixed to stick things inside a scroller that has any transform applied (nor can any of its ancestors have a similar property that creates a new context). That's a browser-imposed limitation, totally unrelated to ScrollTrigger. 

 

6 hours ago, iDad5 said:

The pin-spacer is the only element that actually scrolls, as all its contents are fixed.

Pin spacers aren't scrolling wrappers, they just pad out and add space to scroll into.

 

Quote

I will make some tests with pinSpacing true and a numeric value as the end point for ScrollTrigger, but I'm not sure if that will break other tings in my project

Rather than adding into your project you could add complexity into the codepen until it breaks, that's also a good way to pinpoint the issue.
 

  • Like 1
Link to comment
Share on other sites

1 hour ago, Cassie said:

Pin spacers aren't scrolling wrappers, they just pad out and add space to scroll into.

I have to admit that I doubted that statement at first, as there is no other Element that can move/scroll in the page. Furthermore, when I change the height of the pin-spacer that is the only thing that influences the scrollbar. But when I log the scroll position of the pin-spacer or look at its scrollTop in the dev-tool's properties, it is not moving. Actually, the documentElement is the one that scrolls… — because of its height set to 100%. But I think this more of an academic question.

 

What I did is making two simple versions of the project with the learnings of this thread.

 

I cleaned up the body version a bit and made a window version with 'translate' that also works. 

 

I did this because of my trouble with iOS/iPadOs (I haven't tested on Android yet, but I guess it might be differently trick but tricky nonetheless.)

 

And I'm going CRAZY now.

 

Both versions are basically unusable on the iPhone and on the iPad.

 

The Body version runs somewhat ok, especially regarding the fact that the images I use are enormously large (on purpose for now).

But ScrollTrigger refuses to show the last panel. It always jumps back to the third one.

 

When logging, it just never reaches the last snapToggle and onLeave event it simply snaps back to the 3rd panel.

 

The expected and usual snap event sequence for one scroll down is:

 

snap toggle (without interaction at the start)

snap complete (reaching the second panel)

snap complete (3rd panel)

snap toggle (last panel)

 (then on leave is fired) 

and snap complete for the last panel.

 

The last three are never fired in iOS.

Instead after reaching (visually) the last panel it snaps back to the third panel, firing its snap complete event.

 

Here is no way I can make the last panel stick, even if I only scroll very cautiously a millimeter to the top with panel 3 snapped in the scroll to panel 4 starts but at the end returns to panel 3. 

 

The Window version with transform is running far worse, trembling a lot.

 

It does snap to the last panel, but not playing the animation scrolling away the 3rd panel and just snapping in the last.

 

Also in iOS even more 'wrong label reporting'* is going on than with the transform version, while in the body version this seems to work good now. 

 

(I set overscroll-behavior to none for the HTML and the body (and as a test for all elements) but that does not solve the issue.)

 

I feel like jumping from the balcony right now, as I don't see any feasible solution to make it work on mobile. It used to work, even though it never was 'perfect', but now it is simply unusable. I am very certain that it is Apple that f..ed up everything and likely Android to with the whole “expanding interface” c...p and other non-standard 'optimizations' in scroll-behavior. 
I spent days now trying to save my project on mobile, but I might have to resort to a non ScrollTrigger version for mobile.

 

 

* (wrong label reporting) I need a way to know when which panel has snapped into. The way to get that information (as suggested, if I recall correctly, by Jack himself) is to check for the animations current label property onToggle and onSnapComplete. This use to be incorrect a lot in the past, but seems to work correctly now with the fixed/body version of ScrollTriggger. It still reports wrong sometimes on desktop with the transform/window version but rarely—on iOS it still happens a lot, though.

 

 

Link to comment
Share on other sites

I forgot to mention, that updated the two codePens to work. 

But as codePens are not very good for mobile testing in my experience, I also made two local versions: 

Body: https://dev.thewebworks.de/playground/panels/

Window: https://dev.thewebworks.de/playground/panels/1.html

 

And now, I didn't use ScrollTrigger.normalizeScroll() I have tried it quickly on the local versions, and it seems to break the windowed version completely. The Body version jitters a lot now when snapping, but sometime makes the last panel stay—not always, though.

I will dig deeper after some fresh air...

 

Thank you very much for all your support! 

Link to comment
Share on other sites

57 minutes ago, iDad5 said:

I feel like jumping from the balcony right now, as I don't see any feasible solution to make it work on mobile. It used to work, even though it never was 'perfect', but now it is simply unusable. I am very certain that it is Apple that f..ed up everything and likely Android to with the whole “expanding interface” c...p and other non-standard 'optimizations' in scroll-behavior. 

Apple has butchered it for sure. I spent quite literally many hundreds of hours wrestling with iOS, hunting down obvious browser bugs (some of which have been acknowledged by Apple for YEARS and still not fixed), experimenting with workarounds, etc. normalizeScroll() is the result of those efforts, but it's not a silver bullet. There are simply things that CANNOT be worked around in Apple's browsers. It's absolutely maddening. We've reached out many times directly to them with almost no positive results. Very disheartening for me personally.

 

I don't have time to read through everything in this thread right now. If you want the best chance for a prompt answer, please post the most minimal possible demo with a singular challenge like "can you make the 3 sections pin smoothly on iOS?"  or "my custom scroller is jittery pin-wise here...is that fixable on iOS?" 

 

The shorter the description the better. No need for lots of background information and a list of all the various things you've tried, each result, or loads of console.log() messages, etc. Just super-simple minimal demo that very clearly shows the problem, and a "how can I ____?"  It's just way faster to glance at a minimal demo with a clear goal than read through paragraphs of descriptive text, especially with my foggy COVID brain :)

 

And to manage expectations, I don't have a lot of hope that I'll be able to deliver a silver bullet to get iOS not to jitter. The browser is fundamentally flawed and one of the Apple engineers told me that it's impossible to synchronize things properly scroll-wise. They have no plans to fix that as far as I know. 😡

  • Like 1
Link to comment
Share on other sites

Thank you!

You are a hero, and I'm a big fan of yours. 

You and your team are an inspiration and a great example of how things could and should be. 

Alas, as much as I like to have fun experimenting and making things generate unique experiences, the whole scroll 'abuse' thing somehow felt wrong to me from a usability and accessibility perspective early on. Not your fault at all, you just technically made work things a lot better than others doing it worse. 

Probably I will stop (ab)using scrolling for the time being, I'll sleep over it.  

  • Like 2
Link to comment
Share on other sites

So to salvage things for the moment, I resolved not to use ScrollTrigger on mobile but to go with Observer and move the timeline accordingly.  After all, that would 'more correct' as nothing really scrolls. 

But I still couldn't stop wondering why the solution with the body/fixed scroll was not reaching the end.

 

So, before starting to rework things as described, I did some more testing concerning the end value of ScrollTrigger.

 

Changing from my method of generating the scrollable distance by multiplying my triggers' height and using: pinSpacing: false, end: 'bottom bottom'

to not multiplying my triggers' height and using: pinSpacing: true, end: multipliedHeightAsNumber

 

solved the problem (of not reaching the end on mobile) to my utter astonishment.  

 

As a side effect, I do get more 'wrong label reporting' now again—but I'm used to handling those.

 

@GreenSock I know that's a lot of descriptive text again, sorry. If you deem the issue worth investigating deeper, I will build a minimal demo ;-) or two. But I guess you have better things to do, relaxing and recovering your health probably the most important for you and all of us that depend on your fantastic work.  

Link to comment
Share on other sites

Sure, if you think there's a problem we need to look into related to ScrollTrigger, please do create a super minimal demo that clearly illustrates the issue and I'll take a peek. Otherwise if it's a moot point and you don't actually think it's some kind of bug in ScrollTrigger...great, we can skip it. :)

 

Glad to hear you found a solution for the challenge you were facing. 

  • Thanks 1
Link to comment
Share on other sites

That’s hard for me to judge. 
In my opinion the two approaches I described above should behave the exact same way - and to 99% they do.

 

If I had done the programming I would guess I had to look into it. But I‘m not half as good as any of you, and it’s likely that there is a perfect reason for why one works and the other does not. 
 

The problem of the wrong label reporting I‘d consider an issue that could be worth looking into, but I don’t know how to consistently reproduce it, and it seems not a big issue. 
 

I will make two very simple demos for the two approaches as soon as I have fixed my live issues. 
And if the different behavior is reproduceable in a simple demo, I’ll post them here. 
In case the issue is only occurring in my more complex setup I will let you know. Ok? 

  • Like 1
Link to comment
Share on other sites

Strange behavior number 1 is:

 

I have two ways of creating my ScrollTrigger:

 

A:

const fullHeight = numberOfPanels * heightOfWindow;
gsap.set(container, {height: fullHeight});
var trigger = ScrollTrigger.create({
	trigger: container,
	start: 'top top',
	end: 'bottom bottom',
	pinSpacing: false,
  ...
}

B: 

const fullHeight = numberOfPanels * heightOfWindow;
var trigger = ScrollTrigger.create({
	trigger: container,
	start: 'top top',
	end: fullHeight,
	pinSpacing: true,
  ...
}

 

Both versions act absolutely the same everywhere but in iOS. In iOS Version A scrolls to the last panel, never saps to it though, and snaps back to the one before last. Version B sticks to the end as expected. (At least it did with my local version, with the codePen to follow, it sticks, but reports an endless loop of toggle and leave calls.)

 

Version A:

 

Version B:

See the Pen yLqwXBp by iDad5 (@iDad5) on CodePen

 

I know, they are not super simple yet, but when I created a version even simpler, even Version A worked.

 

I would very much appreciate if someone could check out if the described behavior with the codePens can be reproduced on other devices than the two iPhones and two iPads I tested on.

 

 

Link to comment
Share on other sites

1 minute ago, iDad5 said:

I know, they are not super simple yet, but when I created a version even simpler, even Version A worked.

^^^ this is key

This is exactly how all of us here debug. Simplify down until it works and then play spot the difference. If you provide us with the more simplified version that works it'll really help us to help you!

I'll take a look at this when I'm back at my desk tomorrow, but adding the working demo would be super useful.

Thanks!

Link to comment
Share on other sites

I know, and I am working on getting closer to the behavior from a even more simplified version. 
 

But as the only difference between the two versions is in the three lines I described, the additional Code should not be of real concern. 
I t might be a long shot, but I was hoping that some one would have had a similar Problem and could reduce the need for further work on that (probably) side issue. 

As the behavior is so hard to pinpoint I was also really wondering if it is related to my devices only. 
 

FYI: I could happily go with version B if not an other more severe Problem had occurred. 
With version B ScrollTrigger.scroll(position) does not work as it did with version A before. At least in my Allred very much reduced local test version. 

 

That was what I was actually trying to pinpoint, and I felt that I had to pin down the A B situation as best I could. 
 

Thank you for your patience. 

Link to comment
Share on other sites

It gets more confusing by the minute.

 

I reduced the example as much as possible, without fully losing the chance of tackling the next problem. It is down to around 50 lines of code. But it does essentially the exact same thing as before. 

 

At first, I was surprised that method A worked on iOS—and after some testing, I found that I stopped working when I added:

ScrollTrigger.config(
	{
		autoRefreshEvents: "DOMContentLoaded"
	}
);

 

You can try by commenting in and out lines 22 following here: 

 

See the Pen ZEjPeMQ by iDad5 (@iDad5) on CodePen

 

Then I forked it and changed to method B, fully expecting it to work with and without the above config. 

 

See the Pen OJwqjQB by iDad5 (@iDad5) on CodePen

 

Well, to my total surprise, it does NOT work in both situations.

 

So, while the previous version with more lines of code, but essentially the same programming, does work only in version B and even with the config enabled. The striped-down version works only in version A without the config.

 

I think I need to drink myself to oblivion. 🍺 And finally admit that I'm too dumb for programming. 

  

 

Link to comment
Share on other sites

I'm out and about so I could only skim right now and I'm reading things like "this doesn't work..." but it's not very clear to me what EXACTLY you're expecting and what "doesn't work". @iDad5 I give you permission to explain it as if I was a 5-year old. Please. :) 

 

Like "In this demo, as soon as I configure it to only auto-refresh on DOMContentLoaded, the 2nd panel won't pin at all" (or whatever). 

  1. Minimal demo (looks like you've got that - thanks!)
  2. Super clear steps to reproduce
  3. Description of problem (what you expect, what you're getting)

Thanks! And please don't drink yourself to oblivion, though I completely understand how tempting that is when dealing with Apple browsers. 🙃

  • Like 1
Link to comment
Share on other sites

Greetings Jack @GreenSock from the outskirts of Oblivion. 

 

First and foremost: it is great to hear that you are 'out and about'—sounds a lot better than ill. 

 

The thing with 'super clear steps' in that case is that to me by now nothing feels super clear anymore, but I'll try again:

I do hope you can stay with me, even as it is not what you (and i) wished for. If not, I do understand it and simply will give up with no hard feelings at all – but thankful for your patience always.

 

Let me start with:

 

1. (As far as I can tell) all demos act as expected* on all browsers I tested them on, except for iOS / iPadOS!

(Expected behavior* should be seen by scrolling up and down in any browser not on iOS/iPadOS, namely staying at the last panel when scrolled down to.)

2. To experience unexpected/strange behavior, testing on iPhone or iPad (current OS version) is required.

3. Of the two versions posted in this post, only the one with 'version B' works as expected.

6 hours ago, iDad5 said:

Both versions act absolutely the same everywhere but in iOS. In iOS Version A scrolls to the last panel, never saps to it though, and snaps back to the one before last. Version B sticks to the end as expected.

4. Simply going with version B (

See the Pen OJwqjQB by iDad5 (@iDad5) on CodePen

) ceased to be an option when this version did show unexpected behavior (simply not working) in all browsers and systems. When I used  ScrollTrigger.scroll(value)  as, I tried to go with this approach in a very simplified version of my project. (Let me name it the main problem—which is not yet described and investigated further)

 

FULL STOP.

 

I do understand what @Cassie and @GreenSock keep repeating at me. I've been in your shoes countless times! Gave the same advice too.  But there can be problems that are innately ones of complexity, and to me, this seems to be one of them. So, as hard as I try—and please believe me I do—I'm simply not able to reduce it to more simplicity.

____

 

Heating your advice, I put aside investigation of the “main problem” for now, and tried to narrow down or 'simple demo' the first problem by simplifying my demo further:

[Simplifying mostly meaning taking out Typescript structuring while the actual programming is nearly identical]

 

And what happened then I tried to describe here. (Please remember, we are back to exclusively iOS/iPadOS faulty behavior here)

 

I rephrase: 

 

The only difference between demo 1 and

See the Pen yLqwXBp by iDad5 (@iDad5) on CodePen

is the difference I describe between version A and version B.

From those two (on iOS) only version B (demo 2) worked as expected*.  Version A always behaves faulty with the last panel/label. (Spoiler: Both always used 'only auto-refresh on DOMContentLoaded');

 

One of the few things, my first more simplified demo (beta 3) did not use was the 'only auto-refresh on DOMContentLoaded'  configuration—as I wasn't interested in refreshing—yet.

 

Surprisingly, hat first demo (beta demo 3) did work as expected* (staying at the last panel when scrolled down to it) on iOS despite using version A.

You (hopefully) can reproduce that by commenting on lines 22 and following in and out in

See the Pen ZEjPeMQ by iDad5 (@iDad5) on CodePen

—testing on iOS/iPadOS. (For me commented out it works in iOS, commented in it never sticks at the last panel/label).

 

[I do not comprehend how that  'only auto-refresh on DOMContentLoaded'  does change behavior (even only on iOS) when no refresh (aside from the initial one) is involved in my testing. (I logged it ;-)  to be certain).]

 

The next step was to fork demo 3 and changing ScrollTrigger creation to method B. Given all prior test (manifested in demo1 and demo2)  I was confident that

See the Pen OJwqjQB by iDad5 (@iDad5) on CodePen

(B version of demo 3) would behave as expected*, like demo 2 does. (Demo 2 was created the same way from demo 1)

 

But for me, demo 4 does never stick to the last panel weather I comment the  'only auto-refresh on DOMContentLoaded' configuration in or out (line 22… | and still only talking about iOS.)

 

I always liked Kafka—but never expected to meet his world in programming, but to me, it feels like I fell into one of his stories with this.

 

As long as I don't understand what makes one version behave as expected and the other one not, I don't see how to continue tackling the 'main problem'. I know that iOS is to blame for the disaster in general. But somehow I doubt that the ghost of Steve Jobs haunts my four devices consistently with evil genius, to torment me. Therefore, I suspect that part of that strange behavior must be either my bad programming or a (in this fringe case) too genius programming in ScrollTrigger. I'm at a total loss, though—once again.

 

 

 

*Expected / unexpected behavior: 

6 hours ago, iDad5 said:

In iOS Version A scrolls to the last panel, never saps to it though, and snaps back to the one before last. Version B sticks to the end as expected.

 

 

Link to comment
Share on other sites

You lost me again in that wall of text, links to 8 different CodePens or previous posts, summaries, rephrasings, references to the "main problem" (verses side problems?), "B version of demo 3", demo 2, Version A, version B, etc. 😬

 

I was hoping for more like "here's one simple CodePen...when I set autoRefreshEvents to only DOMContentLoaded (line 22), snapping won't work at the very bottom (it'll scroll back up)" 

 

Does that summarize it? Sorry, I just have very limited time tonight so I could only quickly scan. 

 

If I understand your dilemma correctly, the problem is that you're telling ScrollTrigger NOT to refresh when the window resizes, thus when the address bar shows/hides on iOS and it completely resizes the page, you now have inaccurate start/end values because no refresh() happened, thus the snapping points are off. Your content changed size (which of course would affect the snapping positions) but you prevented ScrollTrigger from refreshing those, so the snapping kicks in to the wrong spot.

 

Does that help at all? 

 

If that's not the "main problem", please give it another shot in "explain it to a 5 year old" (or an old guy with COVID brain) mode :) 

Link to comment
Share on other sites

3 hours ago, iDad5 said:

Asking for a friend: Is there a way to link to a code pen without it killing being embedded? I feel that is not helpful in this case, as I only wanted to reference those in the text.

It's actually SUPER helpful to have them embedded. I'm not aware of a way to prevent that, sorry. 

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...