Say Goodbye to Intersection Observer Boilerplate
For years, if you wanted an element to fade in when it scrolled into view, you reached for IntersectionObserver. You wired up a callback, toggled a class, and prayed the browser didn't jank. It worked — but it was JavaScript doing CSS's job.
The new animation-trigger property changes that. It's a native CSS primitive that delays a CSS animation until a named trigger fires — whether that trigger is a scroll position, a viewport intersection, or even a DOM event like a click.
.element {
/* Wait for the --trigger to activate, then play forward on enter, reverse on exit */
animation: fade-in 0.35s ease-in-out both;
animation-trigger: --trigger play-forwards play-backwards;
}
That single line replaces a nontrivial chunk of JS. This post walks through the syntax, the timeline-trigger setup, and where this fits (and doesn't fit) in production today.
⚠️ Heads up: This spec is currently in Editor's Drafts and ships only in Chrome 145+. Treat it as experimental until broader support lands.
For a deeper look at the spec context, the original write-up on CSS animation-trigger is the 근거자료.

How animation-trigger Actually Works
Syntax
animation-trigger: none | <trigger-name> <trigger-action> [<trigger-action>];
none— default; animation behaves normally.<trigger-name>— a dashed ident (e.g.--fade-in-trigger) that must match a name defined bytimeline-triggerorevent-trigger.<trigger-action>— what to do when the trigger activates (and optionally deactivates).
The Action Keywords
| Action | Behavior |
|---|---|
play-forwards | Positive playback rate; plays forward |
play-backwards | Negative playback rate; plays in reverse |
play | Plays at current rate |
play-once | Plays only from initial/paused state; ignores if already finished |
pause | Freezes in place |
reset | Jumps progress to 0 and pauses |
replay | Resets to 0 and immediately plays |
none | No-op |
Some actions are not or-only — you can pair play-backwards on enter with play-forwards on exit for reversible reveals.
Setting Up a Timeline Trigger
You can't use animation-trigger in isolation. You first need a trigger source:
.trigger {
/* Shorthand: name | source | activation-range / active-range */
timeline-trigger: --trigger scroll() contain / cover;
}
Broken into longhands:
.timeline-trigger-name: --fade-in;
timeline-trigger-source: view();
timeline-trigger-activation-range: contain;
timeline-trigger-active-range: cover;
activation-range— when the trigger turns on (e.g.contain= element fully visible in scrollport).active-range— the outer boundary where the trigger stays active. If omitted, defaults to the activation range.- The active range must include the activation range, or the trigger can never turn on.
Unlike background or border, the shorthand order here is fixed. Don't shuffle it.
Wiring It to an Animation
/* Trigger lives on a parent sentinel element */
.trigger {
timeline-trigger: --trigger scroll() contain / cover;
}
/* Animation applies to any number of children */
.text {
animation-trigger: --trigger play;
animation: fade 0.6s ease-out;
}
Because triggers and animations are decoupled, one trigger can drive many elements. Drop a sentinel at the top of a section, and every child animates in together when it enters the viewport — no per-element observers.
Scoping Triggers
By default, trigger names are global. If two elements declare --trigger, the one later in the cascade wins. Use trigger-scope to confine a trigger to a specific DOM subtree — essential for reusable components.

Scroll-Triggered vs. Scroll-Driven: Not the Same Thing
This is the most common point of confusion. They sound alike, they both use scroll, and they solve different problems.
| Aspect | Scroll-Driven | Scroll-Triggered |
|---|---|---|
| Progress model | Continuous, tied to scroll offset | Binary state (on/off) |
| Timing | Scrubbed frame-by-frame with scroll | Fires once, then runs as a normal CSS animation |
| Reversible? | Yes, naturally (scrub back) | Only if you define a deactivation action |
| Typical use | Parallax, progress bars, sticky reveals | Fade-ins, one-shot reveals, attention grabs |
| Key property | animation-timeline: scroll() | animation-trigger: --x play |
In short: scroll-driven = scrub, scroll-triggered = fire. If you need the animation to rewind as the user scrolls up, you want scroll-driven. If you need a discrete "when this enters, play that" moment, you want a trigger.
Caveats You Shouldn't Ignore
- Chrome 145+ only. Firefox and Safari have not shipped it. Any production use today needs a feature query or progressive enhancement fallback to
IntersectionObserver. - Editor's Draft status. The spec can change. Property names, action keywords, and shorthand ordering are not frozen.
- Global trigger scope by default. Easy to collide in large codebases; always pair with
trigger-scopein components. - No
animation-triggerwithout a source. You still needtimeline-triggerorevent-triggerwired up. - Debugging is rough. DevTools support for trigger activation ranges is still maturing.
If you've been maintaining a similar "human-in-the-loop" release pipeline for open-source projects, the same principles of scoping and fallback apply — see the huggingface_hub weekly release pipeline breakdown for a parallel in CI land.

Where This Leaves Us
animation-trigger is the right direction for the web platform: fewer JS observers, more declarative CSS, and a clean separation between what animates and when it fires. The trigger/animation decoupling alone is worth the price of admission — one sentinel, many animated children.
But it's not production-ready. Chrome-only, Editor's Draft, and no graceful degradation baked in. The pragmatic play today is:
- Prototype with it. Get familiar with the syntax now so you're ready when Safari and Firefox ship.
- Feature-query it. Wrap usage in
@supports (animation-trigger: none)and keep yourIntersectionObserverfallback alive. - Scope your triggers. Don't let global trigger names become the next
!important.
What to Learn Next
- Scroll-driven animations (
animation-timeline: scroll()) — the sibling spec you'll want in the same toolkit. trigger-scope— for isolating triggers inside web components.- Event triggers — the spec also supports DOM events, not just timeline triggers.