Skip to content

[scroll-animations-1] View progress contain of a sticky positioned elements on the edges #8298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ydaniv opened this issue Jan 10, 2023 · 22 comments · Fixed by #8703
Closed

Comments

@ydaniv
Copy link
Contributor

ydaniv commented Jan 10, 2023

There's a specific, but somewhat common, use-case of using a container as a subject of a ViewTimeline and use range: contain on it plus making it sticky positioned.

The problem is when this element is set to top: 0 (many times also with height: 100vw), so it should practically reach contain 100% just as it becomes stuck, but then it should keep its position on the scrollport while the scroll continues, so it's essentially still in "contain" mode.

A simple demo of the scene: https://codepen.io/ydaniv/pen/jOpBxxd

Apple's product pages are also notorious for this technique, for example:

The expected behavior is to add the duration in length of the scroll while the container is "stuck" to the total duration and extend the contain range to include that duration as well.

The spec currently says for contain:

100% progress represents the later position at which:

So the proposed change is to include in the prose a "switch case" that includes the definition of sticky so that the effective scroll duration is extended to cover that length.

cc @fantasai @bramus @flackr

@bramus bramus added the scroll-animations-1 Current Work label Jan 31, 2023
@flackr
Copy link
Contributor

flackr commented Jan 31, 2023

When the sticky element size is the size of the scrollport, this does seem very useful and intuitive. We would also update cover, enter and exit to take this into account as well, right?

It's a bit less intuitive when the sticky element is not smaller than the scrollport and the contain range would be larger than the stuck range, however I could see it still being useful.

I'm not quite sure what you mean by include a switch case with the definition for sticky. I think doing this requires that we work out the minimum and maximum sticky offset (including ancestor sticky elements in the case of nested sticky elements). This is roughly equivalent to answering what is the sticky offset when scrolled to the max scroll offset - and using that offset for the start / end positions.

@flackr
Copy link
Contributor

flackr commented Jan 31, 2023

Note, this gets even more complicated if view timeline has to take into account transforms, as the sticky offset is applied based on the layout position (which doesn't include transform) so it may actually move in the opposite direction of scrolling. here's an example: https://jsbin.com/senacun/edit?html,css,output . I suspect that view timelines should operate on the layout position just like position: sticky does. I think this may already be implied by the fact that the spec already uses the principal box which is the same one used for positioning so I filed a bug for chromium's implementation which does include the transform.

@flackr flackr added the Agenda+ label Jan 31, 2023
@ydaniv
Copy link
Contributor Author

ydaniv commented Jan 31, 2023

We would also update cover, enter and exit to take this into account as well, right?

Well, if I'm not mistaken, cover doesn't have an edge case here. If the element is stuck somewhere in the scrollport then we're still during cover. Unless you mean from implementor point of view (and polyfill of course)? To make sure calculation of effective scroll length includes the stuck duration?
Same goes for enter and exit, I don't think we have edge cases here that requires special attention, right?

It's a bit less intuitive when the sticky element is not smaller than the scrollport and the contain range would be larger than the stuck range, however I could see it still being useful.

Yes. If the sticky element is equal in size then contain==stuck. If the element is larger then it's either still in enter, or at beginning of contain - as in top: 0. In that case I'd expect it would be prepended to contain and beginning of stuck would be contain 0%. Need also to check how these match with the ranges proposed in #7973 .

And, I'd also consider adding a specific range stuck which should IMO provide a very ergonomic sugar for all these cases.

I'm not quite sure what you mean by include a switch case with the definition for sticky. I think doing this requires that we work out the minimum and maximum sticky offset (including ancestor sticky elements in the case of nested sticky elements). This is roughly equivalent to answering what is the sticky offset when scrolled to the max scroll offset - and using that offset for the start / end positions.

I simply meant to add sort of ifs in the spec to handle these cases. And yes, I guess we need figure out these cases.

I suspect that view timelines should operate on the layout position just like position: sticky does. I think this may already be implied by the fact that the spec already uses the principal box which is the same one used for positioning

Whoa, we were just on that page this week for implementing scroll animations, and I read the definition of principal box but couldn't understand from the spec that it actually means excluding transforms, so I did expect it to behave as it is currently in blink.
On one hand, yes, it makes it trickier for authors when interacting with transforms, kind of like:hover is. So this will simplify it. But then your effective view ranges won't match the ones calculated by the timeline. Take for example a simple parallax effect, simply using cover will stop animating somewhere in the middle of the range.

@flackr
Copy link
Contributor

flackr commented Jan 31, 2023

To make sure calculation of effective scroll length includes the stuck duration?

Yes, we need to make sure that as you scroll you make progress from when the element enters the cover range to when it leaves. The current behavior would I believe re-evaluate the cover range around the current sticky position so you'd get an animation that wouldn't progress as you scroll - it would just stick at a given progress.

Whoa, we were just on that page this week for implementing scroll animations, and I read the definition of principal box but couldn't understand from the spec that it actually means excluding transforms, so I did expect it to behave as it is currently in blink. On one hand, yes, it makes it trickier for authors when interacting with transforms, kind of like:hover is. So this will simplify it. But then your effective view ranges won't match the ones calculated by the timeline. Take for example a simple parallax effect, simply using cover will stop animating somewhere in the middle of the range.

I've seen too many cases to count where developers using this feature stumble over the instability of a view timeline animating transform. @bramus @jh3y @argyleink may have some specific examples. I think it's better to align on a model where transform is strictly a post-layout effect (similar to what we've done with position: sticky).

Also, see my above demo #8298 (comment) for how accounting for transform dramatically complicates the range of sticky position.

Could you give an example of this simple parallax effect? How would it not be unstable if it is observing its transformed position and animating its own transform? E.g. here's a demo where you can observe lots of flickering in chrome canary with experimental features on due to this: https://jsbin.com/qojohiw/edit?html,css,output

@bramus
Copy link
Contributor

bramus commented Jan 31, 2023

Need also to check how these match with the ranges proposed in #7973 .

These are extra ranges that apply to the #container element as it’s taller than the scrollport, but they won’t help you here as the animation needs to run:

  • From #container at enter 100%
  • To #sticky at exit 100%

It is currently not possible to spread one set of keyframes and match each part to a different view-timeline to track.

This is the closest I got using the current possibilities but it’s not perfect, as the animation runs until #container is entirely out of view without taking the space #sticky takes up into account.

Thought of using view-timeline-inset: 0 400px; as we know the height of the #sticky – which might not always be the case – but this didn’t seem to work. Could be this is purely an implementation problem in Canary.

Stitching two sets of keyframes together – with one animation watching --container and the other watching --sticky - also won’t work. Ideally, though, I think you’d want something like this:

#container {
  view-timeline-name: --container;
}

#sticky {
  view-timeline-name: --sticky;
}

#sticky img {
  animation: open 1s;
  animation-timeline: auto; /* Just a value to trigger */
}

@keyframes open {
  enter --container 100% {
    clip-path: inset(0 50% 0 50%);
  }
  exit --sticky 100% {
    clip-path: inset(0 0 0 0);
  }
}

But that might just a very tricky thing to implement.

@flackr
Copy link
Contributor

flackr commented Jan 31, 2023

My proposal is that in https://drafts.csswg.org/scroll-animations-1/#view-timelines-ranges when computing the scroll position for the start / end of a range we offset the principal box by its min / max sticky position offset. This should implicitly make everything just work. E.g. imagine you have a box with top: 50px; the start scroll position for cover would be the same (when it first touches the scrollport), but the end scroll position for cover would be beyond the scrolling range because the element would never scroll out of view.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [scroll-animations-1] View progress contain of a sticky positioned elements on the edges, and agreed to the following:

  • RESOLVED: ignore transforms when calculating timeline ranges
  • RESOLVED: the start position for view timeline uses the minimum sticky offset or the offset form the start of the scroll, and the end position uses the max sticky offset
The full IRC log of that discussion <argyle> YehonatanDaniv: when you have a sticky positioned element and this is the subject for the view timeline, it has a top 0 so its effective stack point is both the end of the contained range and the beginning of the exit range
<argyle> YehonatanDaniv: the main issue was around adding the phase of the stickiness to also be included int he contained range, so this would match expectations
<argyle> YehonatanDaniv: later rob had a proposal because more issues were raised on the same issue
<argyle> flackr: so when we were workign out the ranges for all these phases, we rely on the principal box
<argyle> flackr: proposal is that when we try and work out the end of these ranges, we treat sticky elements as if it has it's max sticky offset, the offset that it would get at the end scrollposition
<argyle> flackr: similar for the start value, or the minium sticky offset that it would have at the beginning of scroll
<argyle> flackr: this makes all thephases match the visual expectation of the elements position
<argyle> flackr: which has a related issue that we shouldnt be observing transforms, like other layout primitives
<argyle> fantasai: i think that's a separate topic, lets take them one at a time
<argyle> YehonatanDaniv: also what you wrote, that it's already in the spec, that it was implemented differently
<argyle> flackr: if we dont do this, the proposal for sticky position doesnt work as well because the sticky offset isnt necessarily the same direction as the scroll if you include the transform
<argyle> flackr: then thingd are worse if it includes a transform
<argyle> flackr: should lwe talk about this other thing first?
<argyle> fantasai: want to resolve we ignore transforms then switch back?
<argyle> fantasai: proposed resolution is transforms are ignored when caclculating timeline ranges
<argyle> flackr: major point of frustration, adam and bramus may talk about this as well
<argyle> astearns: it's a frustration that they have to be ignored?
<argyle> flackr: no, that's they're currently not ignored
<argyle> bramus: result now is you get into situations where the entire thing flickers
<argyle> i had to train myself out of this bug
<argyle> flackr: if we could ignore the transform position, it makes devs lives easier. makes sticky position easier to reason about
<argyle> astearns: hearing consensus that we ignore transforms when calculating timeline ranges
<argyle> RESOLVED: ignore transforms when calculating timeline ranges
<argyle> flackr: proposal for this is that the start position for view timeline uses the minimum sticky offset or the offset form the start of the scroll, and the end position uses the max sticky offset
<argyle> astearns: seeing thumbs up, any concerns?
<fantasai> +1
<argyle> RESOLVED: the start position for view timeline uses the minimum sticky offset or the offset form the start of the scroll, and the end position uses the max sticky offset
<fantasai> Not sure how to spec it, but I think I agree with what we should *try* to spec :)
<argyle> astearns: anything else on this issue?
<argyle> YehonatanDaniv: probably better in a separate issue? there were other things
<argyle> astearns: given fantasai's concerns about how to get it specced, let get the spec text then raise issues on that
<argyle> fantasai: it's a matter of, do we have the vocab to talk about this? maybe not, and we need the spec to update to provide that
<argyle> astearns: rob, can you pick something for the remaianing time?
<argyle> astearns: rob, can you pick something for the remaining time?

@fantasai
Copy link
Collaborator

fantasai commented Mar 6, 2023

@flackr @ydaniv Fixed in f353d9a ; I also clarified relative/absolute positioning (in contrast with transforms/sticky). The new text reads:

In all cases, the writing mode used to resolve the start and end sides is the writing mode of the relevant scroll container. Transforms are ignored, but relative and absolute positioning are accounted for. For sticky-positioned boxes, the box’s startmost offset position is used when identifying 0% progress, and the box’s endmost offset position is used when identifying 100% progress. [CSS-POSITION-3] [CSS-TRANSFORMS-1]

If this looks good, feel free to close out the issue as Commenter Satisfied, otherwise lmk if something needs further tweaking. :)

@ydaniv
Copy link
Contributor Author

ydaniv commented Mar 7, 2023

Thanks @fantasai!
I'm not sure I fully understand the wording here, but it could be just me.

What does "offset position" refers to here? The specified inset value?

This issue also aims to standardize how to treat progress of stuck boxes on the edges between ranges, so that the duration of the stuck state is appended to the progress of the earlier range.
Is this also fully covered by the above text?

@flackr
Copy link
Contributor

flackr commented Mar 10, 2023

@fantasai I think may be a bit trickier to specify. We have to work out whether the requested percentage is before or after the point at which the sticky positioned element sticks, and then use the startmost / endmost position respectively.

Consider a few cases with the common css:

.sticky {
  position: sticky;
  top: 10px;
  height: 100px;
  animation-timeline: view();
  animation-range: exit 0% exit 100%; /* expanded for clarity */
}
  1. Sticks below the top of the top of the viewport. In this case, the exit phase shouldn't begin until after the sticky element is no longer stuck as it's not until then that it starts to exit.
.sticky {
  top: 10px;
}
  1. Sticks slightly above the top of the top of the viewport. In this case, the exit phase should begin before the stuck position.
.sticky {
  top: -10px;
}
  1. Sticks at the top of the viewport. In this case, the exit phase should probably begin as soon as the sticky element reaches the stuck position but it is ambiguous because the element remains in that position until nearly the end of the scroll and doesn't exit until then.
.sticky {
  top: 0;
}
  1. Sticks at the top of the viewport, animates to stuck position. In this case, the animation starts before the element sticks. It's ambiguous whether it should end as soon as the element sticks (technically touching the end position) or after the element leaves the stuck position (the first point at which it is no longer at exit 0%).
.sticky {
  top: 0;
  animation-range: contain 99% exit 0%;
}

Cases 3 and 4 are somewhat challenging due to their ambiguity. For animation-range, we could look at whether it's the start or end of the range being asked for and use the startmost / endmost position respectively, however the same ambiguity is not easily solved for keyframes, so I'd suggest we choose a side and apply it consistently. E.g. when the range offset matches the stuck position it always uses the startmost or endmost.

@flackr
Copy link
Contributor

flackr commented Mar 10, 2023

We may be able to avoid having all of this specification complexity in the spec by saying something like e.g. enter 0% is the lowest scroll progress at which the start of the untransformed principal box crosses the end of the viewport. Then we defer to the css-position spec to determine the offset of that principal box for any given scroll progress.

@ydaniv
Copy link
Contributor Author

ydaniv commented Mar 14, 2023

@flackr I think I see what you mean. I had to implement this in JS, and kind of came with the same results - here (currenly only checks for sticky with top).
I had to get initial layout of offsets from the top of the document, and then walk that branch down again and add to the offsets every sticky interval that falls within the animation's duration, or just delay it if it falls before the start.
Perhaps this can help?

@flackr
Copy link
Contributor

flackr commented Apr 4, 2023

I was thinking about this a bit more and I think if we define the 0% of each range as the minimum scroll progress at which the corresponding range constraint is satisfied and 100% as the maximum scroll progress at which the corresponding range constraint is satisfied that it should work for most common use cases.

As an example, consider the following (demo):

<style>
.scroller {
  height: 500px;
  overflow: auto;
  border: 1px solid black;
}
.space {
  outline: 2px solid rgb(0, 0, 255, 0.5);
  height: 500px;
}
.sticky {
  outline: 2px solid rgb(255, 0, 0, 0.5);
  background: yellow;
  position: sticky;
  top: 0px;
  height: 50px;
  view-timeline: sticky;
}
</style>
<div class="scroller">
  <div class="space"></div>
  <div class="space">
    <div class="sticky">Subject</div>
  </div>
  <div class="space"></div>
</div>

The sticky element is unshifted until you scroll down to scrollTop = 500px, and then remains in that position until scrollTop = 950px at which point the sticky element is touching the bottom of its container.

The phases would be as follows:

  • Cover goes from scrollTop = 0px where the subject is touching the scrollport edge to scrollTop = 1000px where due to its sticky offset the element is still touching the top of the scrollport.
  • Enter goes from scrollTop = 0px to scrollTop = 50px as no sticky shift has applied yet.
  • Exit goes from scrollTop = 500px (sticky is just starting to shifted) to scrollTop = 1000px
  • Contain goes from scrollTop = 50px (fully in view) to scrollTop = 950px (last scroll offset at which subject is fully contained due to sticky offset).

@ydaniv
Copy link
Contributor Author

ydaniv commented Apr 4, 2023

@flackr:

Exit goes from scrollTop = 500px (sticky is just starting to shifted) to scrollTop = 1000px

Are you sure you didn't mean:
"Exit goes from scrollTop = 950px (sticky is just starting to shifted) to scrollTop = 1000px"?

Other than that, it's 100%.

@flackr
Copy link
Contributor

flackr commented Apr 5, 2023

This is a good case to dig into. scrollTop = 500 is the lowest scroll progress at which the subject's start border edge coincides with the start edge of its view progress visibility range, so per my definition above it is when it "starts exiting".

However, this is a good argument that perhaps exit and entry should aim to minimize their range which would do what you expect. This would mean that for cover and contain, the range would be as I suggested above, the lowest and highest scroll progress respectively for 0% and 100% in which the corresponding edges coincided. However, for entry, we would use the lowest scroll progress for both 0% and 100% of the range and for exit the highest for both. This is nice because it keeps the alignment of entry / exit with the cover and contain ranges as defined in https://www.w3.org/TR/scroll-animations-1/#view-timelines-ranges

@ydaniv
Copy link
Contributor Author

ydaniv commented Apr 5, 2023

ok, I understand, here you deliberately put the sticky element at 1st pixel of exit. In that case it's fine.
In real life, if that element had a box-sizing: border-box then I'd expect it to stick in contain.
Otherwise it's fine that it's in exit.
Also your definition also seems correct, if I understand it correctly, it makes the ranges mutually exclusive and behavior is more predictable.

@flackr
Copy link
Contributor

flackr commented Apr 5, 2023

ok, I understand, here you deliberately put the sticky element at 1st pixel of exit. In that case it's fine. In real life, if that element had a box-sizing: border-box then I'd expect it to stick in contain. Otherwise it's fine that it's in exit. Also your definition also seems correct, if I understand it correctly, it makes the ranges mutually exclusive and behavior is more predictable.

It's coincident with the exit, there is no border on the subject in the demo so box-sizing: border-box wouldn't change anything. I think the updated definition is good as it makes sense that until the element starts exiting we don't start the exit range (and similarly finish the entry range as soon as it finishes entering).

@ydaniv
Copy link
Contributor Author

ydaniv commented Apr 7, 2023

ok, I took those outlines as borders. But SGTM on the proposal.

@bramus
Copy link
Contributor

bramus commented Apr 7, 2023

Nice demo @flackr. Clearly shows what’s going on.

(To those seeing the contain and exit progress meters do weird stuff in Chrome Canary right now: it’s a bug. The team’s on it. As a workaround, force a main thread animation by adding left: 0px; to the from keyframe block)

@flackr
Copy link
Contributor

flackr commented Apr 12, 2023

@fantasai I linked a PR to try to define it simply as suggested above, would you be willing to review?

@kizu
Copy link
Member

kizu commented May 26, 2023

Update: After a day of experiments I found workarounds for most of my cases, so no action needed for now (though I'd still want to tests a few things, as I still don't completely understand how % works for sticky elements with view timelines)

Original comment I think there might be a need to be able to switch between those two ways the view progress is calculated for the sticky elements. Otherwise, it feels that there is no way to achieve the behavior that could be seen before:

https://codepen.io/kizu/pen/RweORjO — here I would want the sticky elements to have the same exact animation as non-sticky ones — change when they come closer to the top of the viewport. This did work as I did expect in Chrome Canary before the version 116.0.5793.0 in which the behavior did change according to this issue.

Am I missing something, and there is a way to achieve this after this change?

@kizu
Copy link
Member

kizu commented May 26, 2023

Update: After a day of experiments I found workarounds for most of my cases, so no action needed for now (though I'd still want to tests a few things, as I still don't completely understand how % works for sticky elements with view timelines)

Original comment

I did try using the named view timeline on the sticky element's parent, but this doesn't work for my case, as we cannot have a wrapper element for the sticky element without breaking its sticky behavior. So, from what I can gather, it makes it impossible to somehow use the view timeline for the sticky element as if it was not sticky (as in — use it only for the sticky element's original principal box, as if it was not affected by stickiness.

Why I want to have this behavior: it unlocks a lot of different use-cases, like having animated sticky headers, scroll shadows, and so on. Here are some videos of my experiments that did work before 116.0.5793.0 and which seemingly cannot be re-created with the new version of the sticky behavior.

Sticky headers (won't be satisfied by the potential stuck state query or something, as this is not a binary state, but a scroll-driven animation):

sticky-header-example.mov

Scroll shadow (the exact case I have the video of shows a binary state, but I just didn't get to implementing the case where the shadows appear gradually; though unlike sticky shadows there is a chance it could be possible to implement this one with a different method, would need to experiment on it):

scroll-shadow-example.mov

Update: Here is a working version without relying on the view timeline, so this use-case does not count — https://codepen.io/kizu/pen/LYgdgBz?editors=1100

I had experiments that did work in one of the earlier versions of Chrome Canary, and was very happy it would've been possible to achieve these cases with scroll-driven animations, so I would really want us to see if we could somehow make this possible?

Update: I think, I found one workaround that is possible with the current state of the spec — using an entry range and modify it with calc and 100vh — but I think it could still be beneficial to be able to choose which range we want to use for a sticky element — the full range as it is now, or of its original position. — https://codepen.io/kizu/pen/RweORjO?editors=1100

Regardless of the workarounds I found, maybe it is time to revisit the sticky position itself, as there are just too many issues with it (related: #2496), like — could we use anchor positioning to determine what we want the sticky element be sticky to or something? This way we could use the static wrapper with the regular view timeline, and just skip it via anchoring the sticky element to the top of the scrollable container. Or maybe this is a case for reparenting?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants