CSS Scroll-Triggered SVG Animation: 2026 Production Guide
Use timeline-trigger to define when an SVG section becomes active and animation-trigger to start a normal time-based CSS animation. In Chrome 146 and later, this creates a CSS-only entrance without a scroll listener. Keep the SVG visible by default, add an IntersectionObserver fallback, and respect reduced motion.
This guide builds that production pattern from start to finish. The example draws an SVG chart line once when the figure reaches the viewport, reveals two points, remains meaningful without motion, and stays visible if either CSS or JavaScript fails.
What is a CSS scroll-triggered SVG animation?
A CSS scroll-triggered animation is a normal time-based CSS animation whose playback action begins when scrolling moves a timeline into a defined range. Once triggered, its progress is controlled by time, not by every subsequent scroll position.
That distinction matters. The CSS Scroll-driven Animations specification separates scroll-driven effects, whose progress follows the scroll offset, from scroll-triggered effects, which begin at a scroll position and then continue on a clock. The distinction is defined in the current CSS Scroll-driven Animations draft.
For example, a reading progress bar should usually be scroll-driven because its width represents how far the reader has moved through a page. A logo, diagram, or chart that should play a 700-millisecond entrance after it comes into view is usually scroll-triggered.
If you need a broader introduction to choosing between native CSS, Intersection Observer, and animation libraries, begin with Scroll-Triggered SVG Animation: Where to Start. This guide focuses specifically on the newer CSS trigger model and its production fallback.
What is supported in 2026?
As of July 2026, native timeline-trigger, animation-trigger, and trigger-scope support is still new and not cross-browser.
The authoritative Chrome 146 stable release notes, dated March 10, 2026, list scroll-triggered animations as a shipped feature. An earlier Chrome preview article forecast Chrome 145, but the stable release record makes Chrome 146 the safer practical floor.
Chrome’s current feature material shows the trigger properties in Chrome and Chromium-based Edge, while Firefox and Safari do not yet implement them. Those browsers may support parts of the older scroll-driven animation model, such as animation-timeline, but that does not imply support for animation-trigger.
Do not use a browser-name check. Use a feature query in CSS and CSS.supports() in the fallback script. Feature queries test whether a browser accepts a property and value. As MDN’s feature-query guide notes, they cannot guarantee that an implementation is free of partial-support bugs, so real browser testing remains necessary.
When should you use a trigger instead of a scroll timeline?
Use a scroll trigger when crossing a boundary should start a timed action:
- draw an SVG path once after a diagram enters the viewport;
- play a short logo or illustration entrance;
- reveal chart points in a fixed sequence;
- start an onboarding step when its section becomes visible;
- play an animation forward on entry and backward on exit.
Use a scroll-driven timeline when the visual value should continuously represent scroll progress:
- a page progress indicator;
- a path that fills in direct proportion to reading progress;
- a scrubbed technical diagram;
- a controlled scrollytelling sequence;
- a property that should reverse immediately when the user scrolls backward.
A useful test is to stop scrolling halfway through the effect. If the animation should continue to completion, use a trigger. If it should pause at the current visual state, use a scroll-driven timeline.
Build a production-ready SVG example
The following example uses meaningful inline SVG. The chart remains fully visible in the baseline state, so there is no empty space when JavaScript is disabled or an enhancement is unsupported.
1. Add accessible inline SVG markup
<figure class="svg-scroll-demo" data-scroll-trigger>
<svg
class="svg-scroll-demo__graphic"
viewBox="0 0 320 180"
role="img"
aria-labelledby="signupChartTitle signupChartDesc"
>
<title id="signupChartTitle">Monthly sign-up growth</title>
<desc id="signupChartDesc">
A line rises from 42 sign-ups in April to 86 sign-ups in June.
</desc>
<path
class="svg-scroll-demo__axis"
d="M 32 140 H 290"
/>
<path
class="svg-scroll-demo__path"
pathLength="1"
d="M 42 126 C 96 118 126 92 174 88 S 244 48 282 38"
/>
<circle
class="svg-scroll-demo__dot svg-scroll-demo__dot--one"
cx="174"
cy="88"
r="7"
/>
<circle
class="svg-scroll-demo__dot svg-scroll-demo__dot--two"
cx="282"
cy="38"
r="7"
/>
</svg>
<figcaption>
Monthly sign-ups increased from 42 in April to 86 in June.
</figcaption>
</figure>The accessible name and description explain the chart independently of animation. The visible caption also communicates the result without requiring a user to perceive the path drawing. For more labeling patterns, see the guide to accessible SVG titles, descriptions, and decorative graphics.
The pathLength="1" attribute normalizes the path length. That lets the CSS use 1 for both the dash array and dash offset without calculating the path’s length in JavaScript.
2. Add the baseline, native trigger, and fallback styles
.svg-scroll-demo__graphic {
display: block;
width: 100%;
max-width: 42rem;
}
.svg-scroll-demo__axis {
fill: none;
stroke: currentColor;
stroke-width: 2;
opacity: 0.3;
}
.svg-scroll-demo__path {
fill: none;
stroke: #6d5dfc;
stroke-width: 6;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 1;
stroke-dashoffset: 0;
}
.svg-scroll-demo__dot {
fill: #6d5dfc;
opacity: 1;
transform: scale(1);
transform-box: fill-box;
transform-origin: center;
}
@keyframes svg-line-enter {
from {
stroke-dashoffset: 1;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes svg-dot-enter {
from {
opacity: 0;
transform: scale(0.7);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* IntersectionObserver enhancement for unsupported browsers. */
.io-scroll-trigger .svg-scroll-demo__path {
stroke-dashoffset: 1;
}
.io-scroll-trigger .svg-scroll-demo__dot {
opacity: 0;
transform: scale(0.7);
}
.io-scroll-trigger .svg-scroll-demo.is-visible .svg-scroll-demo__path {
animation: svg-line-enter 800ms ease-out both;
}
.io-scroll-trigger .svg-scroll-demo.is-visible .svg-scroll-demo__dot {
animation: svg-dot-enter 400ms ease-out 480ms both;
}
.io-scroll-trigger
.svg-scroll-demo.is-visible
.svg-scroll-demo__dot--two {
animation-delay: 650ms;
}
/* Native scroll-triggered enhancement. */
@supports
(timeline-trigger-name: --svg-story)
and (animation-trigger: --svg-story play-once)
and (trigger-scope: --svg-story) {
.svg-scroll-demo {
timeline-trigger:
--svg-story
view()
contain 10% contain 45% / cover;
trigger-scope: --svg-story;
}
.svg-scroll-demo__path {
stroke-dashoffset: 1;
animation: svg-line-enter 800ms ease-out both;
animation-trigger: --svg-story play-once;
}
.svg-scroll-demo__dot {
opacity: 0;
transform: scale(0.7);
animation: svg-dot-enter 400ms ease-out 480ms both;
animation-trigger: --svg-story play-once;
}
.svg-scroll-demo__dot--two {
animation-delay: 650ms;
}
}
/* Keep the completed graphic static when reduced motion is requested. */
@media (prefers-reduced-motion: reduce) {
.svg-scroll-demo {
timeline-trigger: none;
}
.svg-scroll-demo__path,
.svg-scroll-demo__dot {
animation: none;
opacity: 1;
transform: none;
stroke-dashoffset: 0;
}
}The unguarded styles are the static baseline. Only browsers that accept all three trigger declarations enter the native @supports block.
Keep animation-trigger after the animation shorthand. The current CSS Animation Triggers specification defines animation-trigger as a reset-only subproperty of that shorthand. A later animation declaration can therefore reset the trigger and make the animation run at page load.
3. Add an IntersectionObserver fallback
const supportsTimelineTrigger =
CSS.supports("timeline-trigger-name", "--svg-story") &&
CSS.supports("animation-trigger", "--svg-story play-once") &&
CSS.supports("trigger-scope", "--svg-story");
const reducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
);
if (
!supportsTimelineTrigger &&
!reducedMotion.matches &&
"IntersectionObserver" in window
) {
const targets = document.querySelectorAll("[data-scroll-trigger]");
if (targets.length > 0) {
document.documentElement.classList.add("io-scroll-trigger");
const observer = new IntersectionObserver(
(entries, currentObserver) => {
for (const entry of entries) {
if (!entry.isIntersecting) {
continue;
}
entry.target.classList.add("is-visible");
currentObserver.unobserve(entry.target);
}
},
{
root: null,
rootMargin: "0px 0px -15% 0px",
threshold: 0.15
}
);
targets.forEach((target) => {
observer.observe(target);
});
}
}The fallback uses one observer for every matching figure. According to the official MDN Intersection Observer documentation, the API reports threshold crossings asynchronously and can observe multiple targets with one configuration.
A threshold of 0.15 asks for approximately 15 percent of the target to intersect. The negative bottom rootMargin shrinks the effective viewport, preventing the animation from starting the instant one pixel appears at the bottom edge.
Calling unobserve() after activation is intentional. This is a once-only entrance, so continuing to observe completed figures adds no value. If Intersection Observer is unavailable, the root enhancement class is never added and the baseline graphic remains visible.
How do timeline-trigger and animation-trigger work together?
timeline-trigger creates the trigger. animation-trigger connects an animation to that trigger and assigns an action.
In the example, the trigger has four important parts:
- --svg-story is the trigger’s custom name.
- view() creates a view progress timeline for the figure relative to its nearest scroll container.
- contain 10% contain 45% is the activation range.
- cover, after the slash, is the wider active range.
The CSSWG defines an activation range as the range that changes an inactive trigger to active. The active range controls how long it stays active before an exit action can occur. The active range must contain the activation range.
The animation uses play-once. In the current specification, that action plays an initial or paused animation but does nothing after the animation has reached its finished state. Scrolling away and returning therefore does not restart the chart.
For a reversible effect, use two actions:
animation-trigger:
--svg-story
play-forwards
play-backwards;The first action runs when the trigger activates. The second runs when it deactivates. Use this only when reversing adds meaning; repeated entrance and exit motion can become distracting.
Why is trigger-scope necessary?
Trigger names are global by default. If a selector creates --svg-story on several figures, a later matching element can otherwise become the trigger that animations resolve.
trigger-scope: --svg-story limits that name to the figure’s subtree. Each path and dot then resolves the trigger created by its own figure. This is essential for reusable cards, charts, or article components that repeat the same CSS classes.
If one figure works but several figures activate together, missing trigger scoping is one of the first things to inspect.
How should reduced motion work?
When a user requests reduced motion, present the completed information without the entrance animation. Do not leave the path erased or the data points transparent.
The MDN prefers-reduced-motion reference describes the media feature as a way to detect a request to minimize non-essential motion. W3C’s guidance for WCAG 2.3.3, Animation from Interactions, specifically identifies scroll-related movement as a possible source of discomfort.
This example handles reduced motion in two layers:
- CSS cancels the native and fallback animations and restores the completed state.
- JavaScript does not install the observer enhancement when the preference is already active.
Motion is also not the only source of meaning. The SVG has an accessible description, and its caption states the result. Review the fuller reduced-motion guide for SVG and UI animation before shipping larger scroll effects.
Performance guidance for scroll-triggered SVG
Native triggers remove the need to execute a scroll event handler for this declarative boundary-crossing pattern. Chrome’s release notes say the user agent can offload the trigger interaction to a worker thread. That is useful, but it does not make every animated SVG property inexpensive.
Keep production effects restrained:
- Use one observer for elements with the same threshold and root.
- Unobserve once-only targets after their animation starts.
- Keep path data and the number of simultaneously drawing paths reasonable.
- Prefer short entrances instead of long scroll-blocking sequences.
- Animate transform and opacity for dense groups when a path draw is not essential.
- Do not add will-change to every SVG element; persistent layers consume memory.
- Test nested scroll containers because view() uses the nearest applicable scroller.
- Measure on a low-powered phone, not only a desktop development machine.
Animating stroke-dashoffset may require painting the stroke. It is appropriate for a focused chart or line reveal, but dozens of complex paths entering together can be expensive. The SVG animation performance guide covers property choice, SVG simplification, and real-device testing in more depth.
Production testing checklist
- Test the native path in Chrome 146 or later.
- Test the observer fallback in current Firefox and Safari.
- Disable JavaScript and confirm the complete SVG remains visible.
- Enable reduced motion at the operating-system level and reload.
- Scroll quickly past the figure in both directions.
- Open the page with the figure already inside the viewport.
- Test browser zoom, narrow screens, and a short viewport.
- Test the component inside any modal, carousel, or nested scroller where it will be used.
- Inspect the accessibility tree and verify the title, description, and caption communicate the result.
- Record a performance trace on representative mobile hardware.
Troubleshooting common problems
The animation starts immediately on page load
First, confirm the browser supports both trigger properties. Then check declaration order. A later animation shorthand can reset animation-trigger. Place animation-trigger after the shorthand in the same rule.
Every repeated SVG starts at the same time
Add trigger-scope to the repeated container. Trigger names are global unless scoped, so identical names can collide across cards or figures.
The trigger never activates
Temporarily simplify the range and test the element in the document viewport. Confirm that an ancestor has not become the nearest scroll container through overflow: auto or overflow: scroll. Also test unusually tall elements, for which the contain range behaves differently from a small card.
The path does not start fully hidden
Confirm that the path has pathLength="1" and that both stroke-dasharray and the initial stroke-dashoffset are 1. Without normalization, those values refer to the path’s authored coordinate system and may expose part of the line.
The fallback flashes before becoming hidden
Keep the default artwork visible and scope all hidden fallback states beneath the root class added by JavaScript. Load the small fallback script with defer rather than waiting for unrelated application code. Avoid hiding the SVG in unconditional CSS, because that creates an empty result when JavaScript fails.
Reduced-motion users still see movement
Place the reduced-motion media query after the native and fallback rules so it wins the cascade. Reset the animation and every initial hidden property, including opacity, transforms, and dash offsets.
Frequently asked questions
Is timeline-trigger the same as animation-timeline?
No. animation-timeline can make animation progress follow a scroll or view timeline. timeline-trigger defines a boundary-based trigger, while animation-trigger starts, pauses, resets, replays, or reverses a normal time-based animation when that trigger changes state.
Can a CSS scroll-triggered SVG animation work without JavaScript?
Yes, in browsers that implement the trigger properties. A production page should still provide either a static baseline or an Intersection Observer fallback while support remains limited. The example provides both.
Should timeline-trigger replace IntersectionObserver now?
Not across a general-audience site yet. Use the native trigger as a progressive enhancement and retain Intersection Observer for unsupported browsers. Revisit the fallback when your own browser analytics show sufficient coverage.
Can it animate an SVG loaded through an img element?
You can animate the outer img element as a box, but page CSS cannot target paths inside an SVG loaded through img. Use inline SVG when individual paths, groups, or points need separate animation.
How do I replay the animation every time it enters?
Use the replay action instead of play-once. Replaying entrances can create substantial repeated motion, so use it for deliberate feedback rather than decorative content that users repeatedly cross while reading.
Do I need GSAP for this effect?
No. Native CSS plus a small fallback is enough for a one-shot path draw and a few timed points. A timeline library becomes more useful when you need complex sequencing, responsive orchestration, advanced controls, or broad consistency across many interaction types. The CSS versus GSAP decision guide explains that boundary.
The production rule to remember
Start with the completed, understandable SVG. Add native triggers only inside a feature query. Scope repeated trigger names. Install an observer only where native support is absent. Finally, restore the completed state for reduced motion and no-JavaScript conditions.
That approach lets you use the newest CSS scroll-triggered animation model without making content visibility depend on it. The animation becomes an enhancement, while the information remains reliable everywhere.
Sources and further reading
Related articles
SVG Animation Not Working? A Production Debugging Playbook
Fix broken SVG animations with a systematic checklist for embedding, CSS, JavaScript, paths, IDs, reduced motion, browser timing, and performance.