Open ydaniv opened 1 year ago
also ping @birtles
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 thatboth
andbackwards
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:
animation-timeline
– because the animation would still run on the document timeline.animation-duration
will still have its meaningGood 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.
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:
running
/paused
as the value changes - sort of play/pauserunning
when the value resolves to true - sort of play/stop, with 2 scenarios:
a. run once
b. re-run every time the value is trueIMO 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.:
.element {
animation-play-state: trigger(in-view 50%, once);
}
or for 2.b.:
.element {
animation-play-state: trigger(in-view 50%);
}
And possibly with rootMargins
, if we follow IntersectionObserver's options:
.element {
animation-play-state: trigger(in-view 50% / 10% 0);
}
Also assuming root
isn't specifiable for now and defaulting to nearest
, which is nearest scroll-container, same for ViewTimeline
s.
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:
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:
.element {
animation-trigger: trigger(view(), entry 100%) once;
}
In general the property might look something like:
animation-trigger: <animation-trigger-timeline
<normal | <length-percentage> | <timeline-range-name> <length-percentage>>?
<once | repeat | alternate>>#
The initial value would match the current behavior that animations "trigger" once when they are defined.
@flackr
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 eitherrunning
orpaused
mean? Willpaused
mean "ignore the trigger and stay paused"? Andrunning
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.
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:
true
false
Then, it could become something along the line of:
animation-trigger: view(block 15%) cover / view(block -10%) cover repeat;
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?).
Also, @flackr , could you please change the tagging to css-animations-2
? I don't have these permissions, thanks!
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
.
I think this is a separate use case here, where you can trigger-in on entry and trigger-out on exit. The initial use-case was to trigger-in and having an option to run the animation only once.
@bramus, yes, once
is probably the default value and first use-case we should start with.
alternate
is interesting, and perhaps rather easy to add.
repeat
introduces the discontinuity on the edges of the range, so is a bit of a problem.
@bramus, @flackr, would appreciate your thoughts on what's suggested so far. Perhaps we can wrap up something that can be presented on the agenda for TPAC?
Syntax:
animation-trigger: <animation-trigger-timeline
<normal | <length-percentage> | <timeline-range-name> <length-percentage>>?
<once | repeat | alternate>>#
With once
and alternate
being more easy to define.
While repeat
is a bit more tricky since it has a discontinuity point.
It would be nice if we could squeeze in a solution for repeat
, current suggestion is to have a syntax with optionally 2 ranges/timelines: first for when switching from false to true, and a second one, that replaces the first once the value is true, for when switching from true back to false, in order to solve the discontinuity issue.
Also, perhaps the could be a restriction on the second range/timeline to make it >=
then the first one.
ping @mirisuzanne
To clarify what I suggested earlier, I think it would be helpful to always have a range (i.e. start and end) for all cases. When the end of the range is not specified it could be some form of infinity / auto value so that most of the time developers would only need to specify a start point.
Then, it becomes easier to rationalize all of the behaviors. The "trigger" occurs whenever a rendering update results in going from outside of the range to inside of the range or vice versa, and the behavior depends on the mode:
once
will play the animation a single time when you enter the range. Subsequent entry / exiting is ignored.repeat
will play the animation every time you enter the range.alternate
will play the animation forward every time you enter the range and reverse the animation every time you exit the range.@ydaniv does having a trigger range not fix the discontinuity issue?
@flackr I’m a bit confused about the proposed range. When I think of a Scroll-Trigger Animation, I think of a certain line that the elements needs to cross, not a range. Say I want to trigger an animation when scrolling past the 1000px scroll offset, would your range then be [1000px, +Infinity]
to indicate that the animation can play while in that range?
About the repeat
value: Wouldn’t that have weird visual results when scrolling down and up again? Take a trigger line at a scroll offset of 1000px with a fade-in animation:
opacity: 1
.opacity: 1
.opacity: 1
when at the 999px scrolll offset, and then jumps back to the animation (which starts at opacity: 0
) when at the 1000px scroll offset. This feels like a glitch.@flackr
To clarify https://github.com/w3c/csswg-drafts/issues/8942#:~:text=however%20maybe%20a%20range%20based%20trigger%20always%20plays%20forward%20when%20you%20enter%20the%20range%20and%20plays%20backwards%20when%20you%20leave%20it%20and%20if%20you%20don%27t%20want%20one%20of%20the%20directional%20trigger%20points%20it%20is%20conceptually%20infinite%20/%20the%20start%20or%20end%20of%20the%20range., I think it would be helpful to always have a range (i.e. start and end) for all cases. When the end of the range is not specified it could be some form of infinity / auto value so that most of the time developers would only need to specify a start point.
Yes, of course. If you don't expect navigation to an anchor then you're fine with just the start.
does having a trigger range not fix the discontinuity issue?
Usually "entrance" animations are usually "from" animations, meaning, they animate to an implicit to
keyframe or an identity transform, etc. So your range needs to account for the element to be visible. Question is, what happens when exiting the range on the same position where the element is visible?
Perhaps we could say there's no change when exiting, but then if you enter again the animation will go back to 0% and play again, just like @bramus described above ☝️ .
My intent was to see if we can allow specifying the exit point a bit further, where the element is out of view, so that a nicer effect can be easily achieved.
@bramus
When I think of a Scroll-Trigger Animation, I think of a certain line that the elements needs to cross, not a range. Say I want to trigger an animation when scrolling past the 1000px scroll offset, would your range then be [1000px, +Infinity] to indicate that the animation can play while in that range?
It's just like scroll vs. view timelines again (: You can set s specific point on the scroll and that's a valid use-case. But usually you'll set a trigger using IntersectionObserver and wait for an intersection, which can better described by view timeline. And the nice extra is that this also fixes the awkwardness of transforms on the same element, that by ignoring them we can reason about the trigger point without worrying about the initial animation frame.
When I think of a Scroll-Trigger Animation, I think of a certain line that the elements needs to cross, not a range. Say I want to trigger an animation when scrolling past the 1000px scroll offset, would your range then be [1000px, +Infinity] to indicate that the animation can play while in that range?
I can imagine use-cases where we'd want to completely skip an animation when jumping over the range, for example if we have a long page with multiple scroll-triggered animations, and are landing over some sections via an #anchor
. In some of these cases you could expect the animation to play when scrolling back up to its range.
Even more, I can imagine the cases for the “range” being very short, like half a screen high, meaning that we'd only want to trigger an animation when we're in the range, for example if we have some kind of “screen” where we'd want to play some animation when that screen is completely in the viewport. Scrolling from either top or bottom, we'd want to trigger the animation only when the screen is completely in the viewport.
I can imagine use-cases where we'd want to completely skip an animation when jumping over the range, for example if we have a long page with multiple scroll-triggered animations, and are landing over some sections via an
#anchor
This can be achieved with what @flackr said above, without a range end the animation will be triggered above, not skipped though. But with once
behavior the element will already be animated.
ven more, I can imagine the cases for the “range” being very short, like half a screen high, meaning that we'd only want to trigger an animation when we're in the range, for example if we have some kind of “screen” where we'd want to play some animation when that screen is completely in the viewport. Scrolling from either top or bottom, we'd want to trigger the animation only when the screen is completely in the viewport.
IIUC this should be covered using view()
or at least referencing another named view-timeline.
Usually "entrance" animations are usually "from" animations, meaning, they animate to an implicit
to
keyframe or an identity transform, etc. So your range needs to account for the element to be visible. Question is, what happens when exiting the range on the same position where the element is visible? Perhaps we could say there's no change when exiting, but then if you enter again the animation will go back to 0% and play again, just like @bramus described above ☝️ . My intent was to see if we can allow specifying the exit point a bit further, where the element is out of view, so that a nicer effect can be easily achieved.
I think I understand now, you want a point before which you can safely restore the before state without it being visible to the user. E.g. to put it in concrete terms maybe the start trigger is on entry 100%
but the exit trigger would be at entry 0%
(where the element isn't visible).
I made a crude polyfill of this at https://flackr.github.io/web-demos/scroll-triggered/scroll-triggered.js . I'm hoping this can be helpful in putting together concrete demos.
Demo using it: https://flackr.github.io/web-demos/scroll-triggered/
Demo using it: https://flackr.github.io/web-demos/scroll-triggered/
Nice!
Looking at the code, I see this:
animation-trigger: view() alternate contain 0% contain 100%;
I assume this would also work, because contain
expands to contain 0% contain 100%
:
animation-trigger: view() alternate contain;
Same with this below, which expands to entry 100% normal
:
animation-trigger: view() alternate entry 100%;
Am I correct in this?
Same with this below, which expands to
entry 100% normal
Now that you mentioned it, and also looking back at what @flackr wrote above:
When the end of the range is not specified it could be some form of infinity / auto value so that most of the time developers would only need to specify a start point
We don't have a way for ranges of view timelines to reference the full scroll timeline, and the default for end range is normal
, so we don't currently have this "infinity" behavior.
Demo using it: https://flackr.github.io/web-demos/scroll-triggered/
Nice!
Looking at the code, I see this:
animation-trigger: view() alternate contain 0% contain 100%;
I assume this would also work, because
contain
expands tocontain 0% contain 100%
:animation-trigger: view() alternate contain;
Same with this below, which expands to
entry 100% normal
:animation-trigger: view() alternate entry 100%;
Am I correct in this?
No, because contain 100% is not the same thing as the infinite behavior (anything after the start point) that the polyfill implements. If we want to support a single trigger point that is active for any position after it we need to have different behavior from the default animation range single value.
Same with this below, which expands to
entry 100% normal
Now that you mentioned it, and also looking back at what @flackr wrote above:
When the end of the range is not specified it could be some form of infinity / auto value so that most of the time developers would only need to specify a start point
We don't have a way for ranges of view timelines to reference the full scroll timeline, and the default for end range is
normal
, so we don't currently have this "infinity" behavior.
The polyfill implements this by changing the "fill" behavior of the implicitly created animation. We could implement different behavior for scroll trigger as I do think a common case will be to want a single trigger point forever.
As for ways to represent this, animation-iterations
supports an infinite
value, we could support the same for animation-range. Alternately, we have talked about having a phase for the entire range of the scroller in other issues (https://github.com/w3c/csswg-drafts/issues/8672#issuecomment-1545980510 and https://github.com/w3c/csswg-drafts/issues/8578#issuecomment-1540705525) but I had a hard time getting consensus on this being the range when no range name was specified.
@ydaniv I addressed the exit case https://github.com/w3c/csswg-drafts/issues/8942#issuecomment-1720905866 by parsing up to 4 range triggers. The syntax in the polyfill is currently:
animation-trigger:
<single-animation-timeline>
<once | repeat | alternate>
<animation-trigger-range-start>
<animation-trigger-range-end>?
<animation-exit-range-start>?
<animation-exit-range-end>?
If exit range is unspecified it uses the same as the trigger range. The exit range provides a different range outside of which will cancel / restart repeat
animations or reverse alternate
animations.
The repeat example in the demo makes use of this to avoid seeing the flash away:
.repeat {
animation-trigger:
view() /* A view timeline on the element itself. */
repeat /* Triggers the animation every time the trigger range is entered */
contain 0% contain 100% /* Triggers the animation on entering the contain range. */
cover; /* Cancels and prepares to trigger again on leaving the cover range. */
}
thanks @flackr! This looks good!
There's yet another use-case I'd like to consider: switching animation-play-state
for animations with iterations: infinite
.
This kind of animations could ideally just start playing immediately and keep playing indefinitely, but have to be paused outside the viewport for sake pf perf optimization. Furthermore, none of the current modes we have so far really matches the required effect, unless the author wishes to restart the animation each time, and for that they can use repeat
.
We could name it something like toggle
? Or toggle-state
?
The polyfill implements this by changing the "fill" behavior of the implicitly created animation. We could implement different behavior for scroll trigger as I do think a common case will be to want a single trigger point forever.
Doesn't sound right to me. I don't think we have anything else that behaves this way (as in same property and value inflate differently depending on context) right?
As for ways to represent this, animation-iterations supports an infinite value, we could support the same for animation-range. Alternately, we have talked about having a phase for the entire range of the scroller in other issues (https://github.com/w3c/csswg-drafts/issues/8672#issuecomment-1545980510 and https://github.com/w3c/csswg-drafts/issues/8578#issuecomment-1540705525) but I had a hard time getting consensus on this being the range when no range name was specified.
I like the scroll
range proposal. Although it's a bit limited since view()
is limited to nearest
scroller only, but can be worked around with a named range on a separate element.
There's been a lot of discussion on this issue, can someone summarize where it's at?
There's been a lot of discussion on this issue, can someone summarize where it's at?
What @flackr suggested here is what’s currently suggested. The values once
, repeat
, alternate
are explained in this comment.
Here’s a few tests I created using Rob’s polyfill. These should help you better understand what these values do:
once
: CodePenalternate
: CodePenalternate
with exit-range: CodePenrepeat
: CodePenrepeat
with exit-range: CodePen(Note the page sometimes needs a refresh to work correctly. Also requires a browser that does scroll-driven animations):
And here’s some practical demos:
75vh
measured from the top of the viewport. I thought this would be possible by setting the range to entry calc(0% + 25vh)
but I couldn’t get this to work. The trigger point is wrong. Maybe this is simply a limitation of the polyfill.#p1
is at cover 50%
, the red ball changes color.#p1
is in view, grow with #p2
, shrink with #p3
, …), but currently not supported by the polyfill.view-timeline
of the relevant paragraphs.My findings with doing these tests and building these demos are that:
once
and alternate
work as expected and the ranges allow me to finely control what should happen when.repeat
is a bit weird as it doesn’t animate but jumps when not defining a animation-exit-range
(or when that value is not cover
). I think it’s better to use alternate
here.<animation-trigger-range-start>
to <animation-trigger-range-end>
and <animation-exit-range-start>
to <animation-exit-range-end>
makes sense on paper, it’s a bit hard to mentally wrap yourself around this as the values are crossed when looking at it in terms of scroll direction.
<animation-trigger-range-start>
and <animation-exit-range-end>
(values 1 and 4) are used.<animation-exit-range-start>
and <animation-trigger-range-end>
(values 2 and 3) are used.once
, repeat
, or alternate
.
Thanks @bramus!
repeat
is a bit weird as it doesn’t animate but jumps when not defining aanimation-exit-range
(or when that value is notcover
). I think it’s better to usealternate
here.
Yes, there's a discontinuity point with repeat
, hence the addition of the exit-range
(:
This feels weird, as if the property is overloaded.
I agree the exit-range
makes it look a bit overwhelming, but it is required for solving repeat
.
Otherwise, to make repeat
look good and yet we'll probably need to figure out some magical keyword or rule that solves the discontinuity. OTOH it's very explicit and allows the user to specify exactly what they want without any magic, in the specific case when they need it.
I also asked for adding another type like play-state
(name TBB) for toggling animation-play-state
, mostly for toggling animations with iteration-count: infinite
, would be great to have that as well.
@fantasai current proposed syntax as @flackr wrote above, plus my suggested play-state
is:
animation-trigger:
<single-animation-timeline>
<once | repeat | alternate | play-state>
<animation-trigger-range-start>
<animation-trigger-range-end>?
<animation-exit-range-start>?
<animation-exit-range-end>?
It's probably worth putting together a slide in from left, slide out to right example to explore how easily that is supported. I'm imagining it should look something like this:
@keyframes slide-in-from-left {
0% { transform: translateX(-100vw); }
}
@keyframes slide-out-to-right {
100% { transform: translateX(100vw);
}
.target {
animation:
/* slide-in-from-left should be active producing -100vw initially */
slide-in-from-left 500ms backwards,
/* slide-out-right is active after it triggers keeping the element at 100vw. */
slide-out-to-right 500ms forwards;
animation-trigger:
/* Slide in at entry 100% (note [1], overlaps when slide-out is triggered). */
view() alternate entry 100%,
/* Slide out at exit 0%. */
view() alternate exit 0%;
}
[1] Note, if you immediately scroll to a position after exit 0%
both animations will be active effectively producing an animation from -100vw to 100vw (i.e. fly from left to right).
Also worth noting without a way to refer to the entire scroll range (#9367) it's awkward to make triggers before a certain scroll position.
E.g. we've been assuming that a single named range does some internal magic to extend to the end of the scroll range, but if I want an animation that is triggered when you're scrolled less than contain 100%, I have to extend the range far enough to pass the start scroll, e.g.
animation-trigger: view() alternate entry calc(0% - 1000vh) contain 100%;
SImilarly, we may need a value to return to the author for the automatically extended end of the range, e.g.
animation-trigger: view() alternate contain 0%;
When inspecting the animation trigger it's end value is supposed to be the end of the scroll, not cover 100%
.
The CSS Working Group just discussed [css-animations-2] Add animation-trigger for triggering animations when an element is in a timeline's range
, and agreed to the following:
RESOLVED: Draft up this proposal into css-animations-2, come back to WG for review
@flackr:
It's probably worth putting together a slide in from left, slide out to right example to explore how easily that is supported. I'm imagining it should look something like this:
@keyframes slide-in-from-left { 0% { transform: translateX(-100vw); } } @keyframes slide-out-to-right { 100% { transform: translateX(100vw); } .target { animation: /* slide-in-from-left should be active producing -100vw initially */ slide-in-from-left 500ms backwards, /* slide-out-right is active after it triggers keeping the element at 100vw. */ slide-out-to-right 500ms forwards; animation-trigger: /* Slide in at entry 100% (note [1], overlaps when slide-out is triggered). */ view() alternate entry 100%, /* Slide out at exit 0%. */ view() alternate exit 0%; }
In the above case I guess it would probably make more sense to use the behavior of alternate
as is with exit.
Although this case is also valid, if I'm not mistaken, it should be done better with repeat
.
E.g. we've been assuming that a single named range does some internal magic to extend to the end of the scroll range, but if I want an animation that is triggered when you're scrolled less than contain 100%, I have to extend the range far enough to pass the start scroll, e.g.
You could use a named timeline, or extend the view()
with inset
s, or even use scroll()
. I guess you have many options for that.
When inspecting the animation trigger it's end value is supposed to be the end of the scroll, not
cover 100%
.
I'm not following you here, why don't we want to get back cover 100%
? I think it's consistent to keep normal
as is and have the default set to cover 100%
instead of scroll 100%
.
You think it's a problem?
@flackr I think we have a small issue with the ranges syntax. Take a look at the following example:
#target {
animation-trigger: repeat view() contain cover;
}
In the above the contain cover
part will resolve to the contain 0% cover 100%
of the default range, with the exit range defaulting to the same range.
But the intent could also be to resolve to contain 0% 100% cover 0% 100%
.
Should we add a /
separator between the two ranges to distinguish them more explicitly?
So it would become:
#target {
animation-trigger: repeat view() contain / cover;
}
And if there's no range on the lefthand side it can be computed to normal
.
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:
IntersectionObserver
s taketransforms
into account, so that messes up the ability to properly know when an element is in viewport when animating from outside of it using transformsThe idea of using an
in-view
selector was dismissed, so another idea is perhaps to extend theanimation-play-state
property to support a sort of "trigger" values instead of onlyrunning
/paused
, and reuse the newanimation-range
values syntax to toggle between these states, which could look like: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 topaused
, and when it'strue
it will change torunning
.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 thatboth
andbackwards
are used for those respectively. Or maybe use specific syntax for distinguishing between the two.cc @bramus @flackr