Re: [csswg-drafts] [css-animations-2] Move scroll and event animation triggers to independent namespace (#12336)

@fantasai and I have been reviewing the event-trigger PR (#12314) and related topics, because the last few times these have come up in a call we felt like we didn't understand the subtleties of the model to actually contribute. On review, we believe the existing model as defined for timeline-triggers is not actually shaped well to be extensible in the ways that event-trigger wants it to be. (Not by any particular failing of the editors! It's reasonably defined for timeline-triggers on their own; predicting the exact needed extensions points when adding new variants is incredibly hard until you actually see the variants.) We think the proposal above to split out `timeline-trigger` and `event-trigger` goes in the right direction, but not far enough.

# Quick outline of the existing model #

* "Normal" animations are driven by a time-based timeline: at the time the animation is applied, an interval is defined on the document timeline (based on the animation's delay, duration, and iteration-count), and then the animation compares the timeline's current time to this interval to determine exactly what progress it represents at any given time.
* The newer 'animation-timeline' property lets you opt the animation into different timelines - currently, scroll and view timelines, which are length-based. Same deal, tho—the scroll-timeline-*/etc properties set up the timeline parameters, and the animation compares some current state to the timeline to know what its progress should be.

## Trigger Details ##

The new trigger abilities are totally separate from these timelines - they instead dictate when an animation should *start* or *stop* or *pause*, etc., rather than driving the animation itself. How the animation actually determines its progress once started is completely unrelated—it can be a time-based or scroll-based animation, regardless of what triggers it's using.

Timeline triggers reference an existing scroll/view timeline, and define a sub-range of that timeline to be a "start range"—the animation starts when the timeline's pointer first enters this range—and another (hopefully overlapping) "exit range", which can better be called the "continue range"—the animation is stopped when the timeline pointer *leaves* this range. (None of this, btw, is clear from the spec, which lacks a clear definition of this model or how the various properties fit into it—something that should be fixed.)

The `animation-trigger-behavior` property then controls a few more aspects of this: whether the "start" trigger only works once or repeatedly, and whether the "stop" trigger causes the animation to *pause* or *reset* (or "reset and switch playback direction").

Event triggers are currently more limited, as described. Whenever a particular event occurs, it causes the animation to start, and sometimes pause (with `behavior:state`) or reset and play from the beginning (with `behavior:repeat`).

## Issues ##

These two do not mesh well together. Timeline triggers package together a "start" and "stop"/"pause" signal into a single trigger, while event triggers don't explicitly start/stop/pause at all, but instead take a contextual action based on what the animation is currently doing. These are probably good defaults! But it means that the animation behavior can't be controlled very explicitly, and you can't combine multiple triggers on a single animation (for example, having an animation start/stop based on a timeline, but pause/unpause based on a button click).

(And, somewhat unrelated, the trigger properties currently lack a corresponding "scope" property to lift their name up higher in the tree for reference, or the ability to define "anonymous" triggers. Animation timelines have both of these, and their model can be followed pretty exactly.)

# Proposal #

Here's a first draft of our attempt at making timeline and event triggers work better together, in a way that should hopefully work well with new types of triggers we define in the future. It uses a collection of `animation-trigger` properties to attach start triggers and end (stop/pause) triggers independently, but with smart defaults that get us the natural behavior we want.

(Note: For simplicity, we are skipping the coordinated-list multiplier for these property syntaxes. Pretend they're all wrapped in `[...]#`. The initial values are listed first.)

## Attaching Triggers to Animations ##

* animation-trigger: <'animation-start-trigger'> [ / <'animation-end-trigger'> ]?
* animation-start-trigger: <'start-action'> || <'start-event'>
* animation-start-trigger-action: once | always | alternate
* animation-start-trigger-event: auto | none | [ self | timeline-trigger() | event-trigger() | <dashed-ident> ]+
* animation-end-trigger: <'end-action'> || <'end-event'>
* animation-end-trigger-action: reset | pause
* animation-end-trigger-event: auto | none | [ self | timeline-trigger() | event-trigger() | <dashed-ident> ]+
* animation-trigger-action: <'start-action'> / <'end-action'>
* animation-trigger-event: <'start-event'> / <'end-event'>

For an event of `self`, if an anonymous (`*-name: none`) trigger is defined (see below), then apply that trigger.
For an event of `auto`, if the opposite trigger is neither `auto` nor `none`, copy its value. Otherwise apply `self`.

`timeline-trigger()` and `event-trigger()` functions define anonymous versions of the triggers, similar to how `scroll()` defines an anonymous scroll timeline; they just take the shorthand syntax, minus the *-name part.

## Defining Triggers ##

Triggers define both "start" and "stop" concepts, because the two concepts are intrinsically paired most of the time. `animation-start-trigger` and `animation-end-trigger` only actually *use* one of them, though the simplest way to use the `animation-trigger` shorthand reuses the trigger for both.

* timeline-trigger: <'name'>? <'source'> <'range'>? / <'continue'>?
* timeline-trigger-name: none | <dashed-ident>
* timeline-trigger-source: <'animation-timeline'>
* timeline-trigger-range: <'animation-range'> 
* timeline-trigger-continue: auto | none | <animation-range>

For `timeline-trigger-continue`, `none` means no end trigger, i.e. infinite range; and `auto` simply copies '-range'.

* event-trigger: <'event-trigger-name'>? <'event-trigger-type'>
* event-trigger-name: none | <dashed-ident>
* event-trigger-type: <'event-trigger-start-type'> / <'event-trigger-end-type'>?
* event-trigger-start-type: normal | focus | hover | event(DOM event name)
* event-trigger-end-type: none | same-as-start | exit-start | <event-trigger-start-type>
    * `normal` (or `activate`) - Corresponds to whatever activation methods are defined on this element or if none are, then any actions that would activate a button. Roughly (exactly?) corresponds to `:active` (click, tap, certain keypresses while focused).
    * `none` - No end trigger.
    * `same-as-start` - The same trigger action as the start-type, the next time you do it. (This needs a better keyword. `re-enter`? `same`?)
    * `exit-start` - If the start trigger has a duration, then as soon as the duration is exited. Otherwise none. Example: if the start trigger is specified as `focus`, then onblur. (This also needs a better keyword. `leave`? just `exit`?)

## Scoping Trigger Names ##

* trigger-scope (assuming we want all the trigger types to share a namespace)

Thoughts?
~fantasai and TJ

-- 
GitHub Notification of comment by tabatkins
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/12336#issuecomment-3091201201 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Friday, 18 July 2025 22:53:44 UTC