You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[css-animations-2] Add animation-trigger for triggering animations when an element is in a timeline's range #8942
Entry animations are pretty common on the web. Currently in order to trigger time-based animations when an element enters the viewport we have to use IntersectionObserver.
The problems with this:
requires writing some amount of JS for rather simple use-case
requires waiting for JS to start for playing animations (may be slow when sites import a lot of JS)
IntersectionObservers take transforms into account, so that messes up the ability to properly know when an element is in viewport when animating from outside of it using transforms
the web engine is not aware of the fact that the element will be animated on entry and may punish you on perf
The idea of using an in-view selector was dismissed, so another idea is perhaps to extend the animation-play-state property to support a sort of "trigger" values instead of only running/paused, and reuse the new animation-range values syntax to toggle between these states, which could look like:
.element{animation-name: fly-in;animation-duration:1s;animation-fill-mode: backwards;/* magic right here */animation-play-state:toggle(entry 50%);}
A good thing about ranges is that they already ignore transforms for SDA, so the same semantics could apply here and help solve this problem of transforms messing with initial position for triggering on viewport intersection.
While this condition evaluates to false the value computes to paused, and when it's true it will change to running.
Another requirement would be to be able to both toggle every time the computed value changes, and also have it set once. Perhaps this could be inferred from the animation-fill-mode, so that both and backwards are used for those respectively. Or maybe use specific syntax for distinguishing between the two.
The idea of using an in-view selector was dismissed
I'm just curious about what the discussion was here. I know in the very very early days of the scroll-driven animations spec we have scroll triggers which would have been awesome--basically like declarative intersection observer so you could trigger transitions or even just static changes. The main problem was just that you couldn't easily implement them such that they run on the compositor. I think they were probably the same thing as in-view selectors. Is that the reason the in-view selector was dismissed?
Perhaps this could be inferred from the animation-fill-mode, so that both and backwards are used for those respectively.
Fill modes are tricky. They don't compose very well and we had to introduce the whole effect replacing mechanism just to prevent them from leaking memory. At times we've talked about deprecating them if we could. They might be the right primitive here, but just as a knee-jerk reaction, I think it would be worth considering other options before we try to overload them for this.
Is that the reason the in-view selector was dismissed?
There was a specific section on this in the CSSWG wiki but can't find it now cause the wiki is down ):
But essentially it said this would be problematic in cases where you could select based on this state, and then undo that state inside the same rule.
I should note that a sort of an in-view state is also being considered for state() queries, which may also be useful for other use-cases, like for having different elements for checking state and applying style change.
I know in the very very early days of the scroll-driven animations spec we have scroll triggers which would have been awesome--basically like declarative intersection observer so you could trigger transitions or even just static changes
Yes, there was such issue (couldn't find it) but we agreed that "scroll-driven" is technically and conceptually different from "scroll-trigger". So we closed that one.
Having a declarative intersection observer would be great, but since it was stuck I'm trying here to find a way forward and also addressing specific cases for triggering animations.
I think it would be worth considering other options before we try to overload them for this
Understood, they may be completely orthogonal to this case, just trying to consider sensible defaults.
Yehonatan and I discussed this IRL while both at CSS Day. Must say I like the general idea of using animation-play-state for this:
It hints at “this is a regular animation, but it’ll only play from a certain point (in space)”
It correctly assumes to not be a timeline – so doesn’t fit into animation-timeline – because the animation would still run on the document timeline.
It doesn’t require a parent container or anything
animation-duration will still have its meaning
Good remark by Brian on not overloading animation-fill-mode for this. Perhaps the “trigger once” thing could be a flag in the triggering function.
As for bikeshedding: I would name the function trigger() instead of toggle().
FWIW: an alternative idea I had in the back of my mind somewhere was to adjust animation-duration to accept something like trigger(5s, entry 10%) but that seems more convoluted than Yehonatan’s proposal.
Reacted by Yehonatan Daniv, Oliver Williams and Arif
Giving this more thought, using animation-range will not work here since it specifies every point on the range for a continuous event, and what we need here is actually a singular trigger, more like an IntersectionObserver.
Instead of knowing the exact point on the range in each frame we need to resolve a boolean value indicating whether the element is intersecting the viewport or not.
This can be interpreted as 2 possible options:
toggle between running/paused as the value changes - sort of play/pause
trigger a single change to running when the value resolves to true - sort of play/stop, with 2 scenarios:
a. run once
b. re-run every time the value is true
IMO option 1. is less common, and will only have a meaning for long/infinite animations, and it's probably much more interesting to pursue option 2 with both variations.
Naming aside, syntax could possibly looks something like:
In the early days when we discussed this, we recognized that this was not complicated to enable for animations, but we were concerned about how developers could use this to start transitions as it seemed like a natural thing people would want to do. There was a proposal to have a media query style syntax in #4339. See also #7478. e.g.
@scroll-trigger(>500) {
/* style rules to trigger animation or transition */
}
The issue with this is that evaluating this change requires a global style recalculation and as such would be unlikely to be able to be started on the compositor. I'm happy to solve this for animations which if we set up up-front we could dynamically trigger from the compositor. We should think about whether there are any ways we could set this up for transitions as well. E.g. maybe for transitions we could do something similar to @starting-style but associate it with a particular trigger point. Then to composite the transition this style information and the trigger would be passed along to the compositor.
I don't think it's a good idea to re-use animation-play-state for this as you don't want the animation to pause if you scroll past the trigger point and then back before it. Instead you want one of two behaviors:
Play to completion, or
If the animation would trigger in reverse at this scroll threshold, start reversing the animation.
My thinking around this is that it is somewhat similar to animation-delay but repeatable. I'd propose introducing something like an animation-trigger property which would have values for the various modes you may want this to work. The trigger could be a point on an animation timeline which could be time-based or could be scroll based. This might be something like the following:
We should think about whether there are any ways we could set this up for transitions as well. E.g. maybe for transitions we could do something similar to @starting-style but associate it with a particular trigger point. Then to composite the transition this style information and the trigger would be passed along to the compositor.
Transitions, unlike animations, can't use the trigger in a property/value since they are triggered by the cascade itself. Having excluded the option for having a trigger as a selector, I think the only option still being discussed now is using state queries for that in #6402. I think that direction may also prove useful for the more advanced use-case for animations, when you want to separate the triggering element from the animated element.
I don't think it's a good idea to re-use animation-play-state for this as you don't want the animation to pause if you scroll past the trigger point and then back before it. Instead you want one of two behaviors:
Play to completion, or
If the animation would trigger in reverse at this scroll threshold, start reversing the animation.
Yes, these 2 options sound option 2 I mentioned above.
The main problem I see with introducing a new property and not extending animation-play-state is how will they interact with each other? What does having a trigger and having a play-state of either running or paused mean?
Will paused mean "ignore the trigger and stay paused"? And running is "play when defined or when triggered"?
trigger could be a point on an animation timeline which could be time-based or could be scroll based
I'm not sure we need the a point on a timeline. The time-based one is simply a delay, right? And the scroll-based one may be confusing, because what you probably want is a simply intersection, no matter which direction or point on the view-timeline the element is at. E.g: what happens with view(), entry 100% if the page was navigated to in mid-scroll and then user scrolled to reveal the element from the scrollport's start edge? Does it really matter?
I guess if we add something like view(), in 100% and view(), out 100% we could cover all cases.
<once | repeat | alternate>
I really like this part! Kind of binds together all the use-cases (:
Yes, these 2 options sound option 2 I mentioned above.
My apologies, I missed that this was the behavior you proposed for it. I still worry this isn't well described by the paused animation-play-state as it suggests option 1 based on what changing play state to paused usually does. Also when you haven't triggered the animation yet I think it should be in the before phase (as if you had a delay you haven't hit yet), rather than paused at the start and should probably hold the after phase after it finishes. This lets the developer decide whether it should fill or not.
The main problem I see with introducing a new property and not extending animation-play-state is how will they interact with each other? What does having a trigger and having a play-state of either running or paused mean? Will paused mean "ignore the trigger and stay paused"? And running is "play when defined or when triggered"?
If you think of this as similar to animation-delay, the animation is just "running" the whole time but it's in the before phase until triggered (i.e. only producing an effect if you have fill: backwards). If you set it to paused the animation would I suppose start paused when the trigger is passed holding the first keyframe until you unpause it.
I'm not sure we need the a point on a timeline. The time-based one is simply a delay, right?
When given a document timeline yes. I was thinking in the long run you might pass it a point on a (yet to be defined) video timeline or other such time-based triggers which are not simple delays.
And the scroll-based one may be confusing, because what you probably want is a simply intersection, no matter which direction or point on the view-timeline the element is at. E.g: what happens with view(), entry 100% if the page was navigated to in mid-scroll and then user scrolled to reveal the element from the scrollport's start edge? Does it really matter?
There are a few ways that a single point could be interpreted.
Whenever you go from one side of the given point to the other you trigger the animation.
Whenever you have a "time" >= the trigger point, trigger the animation.
Whenever you pass the trigger point in a particular direction, trigger the animation.
We could imply all of these options from the proposed once | repeat | alternate. E.g. alternate implies the animation happens every time you cross in the direction you crossed in, and maybe the other two can decide which direction they care about - or maybe they care about the direction determined by the animation-direction?
It sounds like you're suggesting though that you might want an upper limit, such as trigger a particular animation anytime you scroll between 500px and 1000px (such that a particular element is intersecting the viewport). I think in order to avoid timing issues you'd probably want to trigger even if the user scrolled past the range without ever entering it just so that you'd be in a consistent state regardless of scroll speed. I'm a bit worried this could get into an arbitrarily complex syntax space for multiple triggers, repeating triggers etc, however maybe a range based trigger always plays forward when you enter the range and plays backwards when you leave it and if you don't want one of the directional trigger points it is conceptually infinite / the start or end of the range.
I still worry this isn't well described by the paused animation-play-state as it suggests option 1 based on what changing play state to paused usually does. Also when you haven't triggered the animation yet I think it should be in the before phase (as if you had a delay you haven't hit yet), rather than paused at the start and should probably hold the after phase after it finishes. This lets the developer decide whether it should fill or not
Yes, it should hold the fill state until triggered and after finishing, so I guess in that sense it's not really the play-state according to the model, and rather a new thing that toggles phases, as you say.
If you think of this as similar to animation-delay, the animation is just "running" the whole time but it's in the before phase until triggered (i.e. only producing an effect if you have fill: backwards). If you set it to paused the animation would I suppose start paused when the trigger is passed holding the first keyframe until you unpause it.
Yep, that sounds good.
I was thinking in the long run you might pass it a point on a (yet to be defined) video timeline or other such time-based triggers which are not simple delays.
Hmm, yes, +1 on that.
It sounds like you're suggesting though that you might want an upper limit, such as trigger a particular animation anytime you scroll between 500px and 1000px (such that a particular element is intersecting the viewport).
Yeah, sort of, you helped realize what I actually wanted is a range in which the computed value is a boolean, rather than progress. So inside it it's true and false outside. And a single toggle-point can be simply from range start to infinity. And these cover well scroll/view/hover/time/etc.
or maybe they care about the direction determined by the animation-direction?
I think that defines the initial direction.
I'm a bit worried this could get into an arbitrarily complex syntax space for multiple triggers, repeating triggers etc
We may probably want to add multiple triggers, but I'd be happy to solve the single case first (:
@flackr there is a problem with repeat, as it creates a non-continuous point on the timeline's edges.
For example take an element with view() entry 50% exit 50% repeat, and the user is scrolling just beyond one of the 50% points, where the trigger value is flipped back to false, and that should reset the animation back to before phase. Or maybe just if stopping just beyond the 50% point and then scrolling back into the range it should reset back and trigger again.
Either way we get a visual jump.
I think it's something that needs a proper handling OOTB. Only thing I can think of now is allowing multiple triggers, and then the author can add another negated (false inside the range) trigger for reset.
WDYT?
I think another way to tackle this is by imagining how this would be implemented today:
You'd have 2 IntersectionObservers: one for triggering on entry (isIntersecting=true) that flips the value to true, and a second one for triggering on exit (isIntersecting=false), with a range that has to be larger than the first one's, that flips the value to false.
So you end up with 2 ranges:
range for entry to flip to true
range for exit to flip to false
Then, it could become something along the line of:
Activity
ydaniv commentedon Jun 10, 2023
also ping @birtles
birtles commentedon Jun 12, 2023
I'm just curious about what the discussion was here. I know in the very very early days of the scroll-driven animations spec we have scroll triggers which would have been awesome--basically like declarative intersection observer so you could trigger transitions or even just static changes. The main problem was just that you couldn't easily implement them such that they run on the compositor. I think they were probably the same thing as
in-viewselectors. Is that the reason thein-viewselector was dismissed?Fill modes are tricky. They don't compose very well and we had to introduce the whole effect replacing mechanism just to prevent them from leaking memory. At times we've talked about deprecating them if we could. They might be the right primitive here, but just as a knee-jerk reaction, I think it would be worth considering other options before we try to overload them for this.
ydaniv commentedon Jun 12, 2023
There was a specific section on this in the CSSWG wiki but can't find it now cause the wiki is down ):
But essentially it said this would be problematic in cases where you could select based on this state, and then undo that state inside the same rule.
I should note that a sort of an
in-viewstate is also being considered forstate()queries, which may also be useful for other use-cases, like for having different elements for checking state and applying style change.Yes, there was such issue (couldn't find it) but we agreed that "scroll-driven" is technically and conceptually different from "scroll-trigger". So we closed that one.
Having a declarative intersection observer would be great, but since it was stuck I'm trying here to find a way forward and also addressing specific cases for triggering animations.
Understood, they may be completely orthogonal to this case, just trying to consider sensible defaults.
bramus commentedon Jun 12, 2023
Yehonatan and I discussed this IRL while both at CSS Day. Must say I like the general idea of using
animation-play-statefor this:animation-timeline– because the animation would still run on the document timeline.animation-durationwill still have its meaningGood remark by Brian on not overloading
animation-fill-modefor this. Perhaps the “trigger once” thing could be a flag in the triggering function.As for bikeshedding: I would name the function
trigger()instead oftoggle().FWIW: an alternative idea I had in the back of my mind somewhere was to adjust
animation-durationto accept something liketrigger(5s, entry 10%)but that seems more convoluted than Yehonatan’s proposal.ydaniv commentedon Jun 20, 2023
Giving this more thought, using
animation-rangewill not work here since it specifies every point on the range for a continuous event, and what we need here is actually a singular trigger, more like an IntersectionObserver.Instead of knowing the exact point on the range in each frame we need to resolve a boolean value indicating whether the element is intersecting the viewport or not.
This can be interpreted as 2 possible options:
running/pausedas the value changes - sort of play/pauserunningwhen the value resolves to true - sort of play/stop, with 2 scenarios:a. run once
b. re-run every time the value is true
IMO option 1. is less common, and will only have a meaning for long/infinite animations, and it's probably much more interesting to pursue option 2 with both variations.
Naming aside, syntax could possibly looks something like:
For 2.a.:
or for 2.b.:
And possibly with
rootMargins, if we follow IntersectionObserver's options:Also assuming
rootisn't specifiable for now and defaulting tonearest, which is nearest scroll-container, same forViewTimelines.flackr commentedon Jun 22, 2023
In the early days when we discussed this, we recognized that this was not complicated to enable for animations, but we were concerned about how developers could use this to start transitions as it seemed like a natural thing people would want to do. There was a proposal to have a media query style syntax in #4339. See also #7478. e.g.
The issue with this is that evaluating this change requires a global style recalculation and as such would be unlikely to be able to be started on the compositor. I'm happy to solve this for animations which if we set up up-front we could dynamically trigger from the compositor. We should think about whether there are any ways we could set this up for transitions as well. E.g. maybe for transitions we could do something similar to
@starting-stylebut associate it with a particular trigger point. Then to composite the transition this style information and the trigger would be passed along to the compositor.I don't think it's a good idea to re-use
animation-play-statefor this as you don't want the animation to pause if you scroll past the trigger point and then back before it. Instead you want one of two behaviors:My thinking around this is that it is somewhat similar to
animation-delaybut repeatable. I'd propose introducing something like ananimation-triggerproperty which would have values for the various modes you may want this to work. The trigger could be a point on an animation timeline which could be time-based or could be scroll based. This might be something like the following:In general the property might look something like:
The initial value would match the current behavior that animations "trigger" once when they are defined.
ydaniv commentedon Jun 23, 2023
@flackr
Transitions, unlike animations, can't use the trigger in a property/value since they are triggered by the cascade itself. Having excluded the option for having a trigger as a selector, I think the only option still being discussed now is using state queries for that in #6402. I think that direction may also prove useful for the more advanced use-case for animations, when you want to separate the triggering element from the animated element.
Yes, these 2 options sound option 2 I mentioned above.
The main problem I see with introducing a new property and not extending
animation-play-stateis how will they interact with each other? What does having a trigger and having a play-state of eitherrunningorpausedmean?Will
pausedmean "ignore the trigger and stay paused"? Andrunningis "play when defined or when triggered"?I'm not sure we need the a point on a timeline. The time-based one is simply a delay, right? And the scroll-based one may be confusing, because what you probably want is a simply intersection, no matter which direction or point on the view-timeline the element is at. E.g: what happens with
view(), entry 100%if the page was navigated to in mid-scroll and then user scrolled to reveal the element from the scrollport's start edge? Does it really matter?I guess if we add something like
view(), in 100%andview(), out 100%we could cover all cases.I really like this part! Kind of binds together all the use-cases (:
flackr commentedon Jun 24, 2023
My apologies, I missed that this was the behavior you proposed for it. I still worry this isn't well described by the paused
animation-play-stateas it suggests option 1 based on what changing play state to paused usually does. Also when you haven't triggered the animation yet I think it should be in the before phase (as if you had a delay you haven't hit yet), rather than paused at the start and should probably hold the after phase after it finishes. This lets the developer decide whether it should fill or not.If you think of this as similar to animation-delay, the animation is just "running" the whole time but it's in the before phase until triggered (i.e. only producing an effect if you have
fill: backwards). If you set it to paused the animation would I suppose start paused when the trigger is passed holding the first keyframe until you unpause it.When given a document timeline yes. I was thinking in the long run you might pass it a point on a (yet to be defined) video timeline or other such time-based triggers which are not simple delays.
There are a few ways that a single point could be interpreted.
We could imply all of these options from the proposed
once | repeat | alternate. E.g. alternate implies the animation happens every time you cross in the direction you crossed in, and maybe the other two can decide which direction they care about - or maybe they care about the direction determined by theanimation-direction?It sounds like you're suggesting though that you might want an upper limit, such as trigger a particular animation anytime you scroll between 500px and 1000px (such that a particular element is intersecting the viewport). I think in order to avoid timing issues you'd probably want to trigger even if the user scrolled past the range without ever entering it just so that you'd be in a consistent state regardless of scroll speed. I'm a bit worried this could get into an arbitrarily complex syntax space for multiple triggers, repeating triggers etc, however maybe a range based trigger always plays forward when you enter the range and plays backwards when you leave it and if you don't want one of the directional trigger points it is conceptually infinite / the start or end of the range.
ydaniv commentedon Jun 29, 2023
Yes, it should hold the fill state until triggered and after finishing, so I guess in that sense it's not really the play-state according to the model, and rather a new thing that toggles phases, as you say.
Yep, that sounds good.
Hmm, yes, +1 on that.
Yeah, sort of, you helped realize what I actually wanted is a range in which the computed value is a boolean, rather than progress. So inside it it's true and false outside. And a single toggle-point can be simply from range start to infinity. And these cover well scroll/view/hover/time/etc.
I think that defines the initial direction.
We may probably want to add multiple triggers, but I'd be happy to solve the single case first (:
ydaniv commentedon Jul 3, 2023
@flackr there is a problem with
repeat, as it creates a non-continuous point on the timeline's edges.For example take an element with
view() entry 50% exit 50% repeat, and the user is scrolling just beyond one of the 50% points, where the trigger value is flipped back to false, and that should reset the animation back tobeforephase. Or maybe just if stopping just beyond the 50% point and then scrolling back into the range it should reset back and trigger again.Either way we get a visual jump.
I think it's something that needs a proper handling OOTB. Only thing I can think of now is allowing multiple triggers, and then the author can add another negated (false inside the range) trigger for
reset.WDYT?
ydaniv commentedon Jul 12, 2023
I think another way to tackle this is by imagining how this would be implemented today:
You'd have 2 IntersectionObservers: one for triggering on entry (
isIntersecting=true) that flips the value totrue, and a second one for triggering on exit (isIntersecting=false), with a range that has to be larger than the first one's, that flips the value tofalse.So you end up with 2 ranges:
truefalseThen, it could become something along the line of:
And the second range is optional of course, and has to be larger than the first range (I think that's possible to validate, right?).
ydaniv commentedon Jul 12, 2023
Also, @flackr , could you please change the tagging tocss-animations-2? I don't have these permissions, thanks![-][css-animations] Extend animation-play-state for triggering animations when an element is in viewport[/-]61 remaining items
Bug 1955380 [wpt PR 51499] - Action scroll-triggered CSS animations, …
Action scroll-triggered CSS animations
Bug 1955380 [wpt PR 51499] - Action scroll-triggered CSS animations, …
Bug 1943054 [wpt PR 50216] - Implement parsing for animation-trigger-…
Bug 1943140 [wpt PR 50225] - Implement parsing for animation-trigger-…
Bug 1945138 [wpt PR 50409] - Parse animation-trigger shorthand, a=tes…
Bug 1955380 [wpt PR 51499] - Action scroll-triggered CSS animations, …