Technology Review · Jason Forrest Agency · August 2026

Animating SVG on scroll

Which animation and physics framework to build a best-in-class visual explainer on — evaluated for a diagram-heavy story with spatial ambition, and for a Figma/Illustrator → Claude → React/Svelte production pipeline.

Scope: 14 toolsVerified 18 Aug 2026Sizes measured from npm tarballs, gzip −9

01The verdict

For a diagram-heavy scroll explainer in 2026, the correct answer is boring and it got cheaper: SVG in the DOM, geometry generated by D3, choreographed by GSAP ScrollTrigger, with everything else brought in surgically and code-split. The interesting news is that this stopped being a compromise — in April 2025 Webflow made every GSAP plugin free for commercial use, which handed the SVG lane the two tools it was always missing: MorphSVG and DrawSVG.

Recommended stack

Spine
SVG + D3 (modular) + GSAP ScrollTrigger / DrawSVG / MorphSVG
~60 KB gzip total. Real text, real accessibility, SEO-visible, screenshot-able, print-able. Pin, scrub and snap are only available here.
Free layer
Native CSS animation-timeline: view()
Reveals, parallax, progress bars. 0 KB, runs off the main thread, ~85% of users. Silently no-ops elsewhere. Use it for everything cheap.
Steps
Scrolleo (ZeitOnline) or hand-rolled IntersectionObserver
Scrollama is still the standard but hasn't published to npm since June 2022. Scrolleo is the maintained near-clone.
Escalate
Rive — for one stateful hero moment · Three.js — for one spatial moment
Both behind a dynamic import() so mobile and reduced-motion users never download them. Rive floor: $9/seat/mo annual. Three floor: ~128 KB gzip.
Skip
Spline, deck.gl, PixiJS, Jitter, Typeflow
Each wrong for this brief for a specific, checkable reason — see below. deck.gl only re-enters if the data turns out to be genuinely geospatial.

The one-line rule that decides most of this: if the explainer has labels, axes, annotations and legible type — and a diagram-heavy explainer does — the renderer must be SVG. Canvas and WebGL runtimes (Rive, PixiJS, Three.js, Spline, dotLottie) are opaque to screen readers, invisible to search engines, cannot be selected or translated, and render text as either SDF glyphs or baked outlines. That's an acceptable trade for one hero moment. It's an unacceptable trade for the body of a public explainer.

02What SVG + scroll actually does

Four live demos, each in its own scroll pane — scroll inside the boxes. All four run on zero libraries: hand-written vanilla JS on a requestAnimationFrame loop, which is exactly what GSAP ScrollTrigger does under the hood, minus the ~48 KB of edge-case handling you'd want in production.

1 · Draw-on-scrollstroke-dasharray + stroke-dashoffset · GSAP DrawSVGPlugin = 2.2 KB gz
↓ scroll inside
Q1Q2 Q3Q4 ↑ inflection

The workhorse move for any explainer that draws a trend, a route, a connection or a process arrow. Requires a live stroke on an open path — if your Figma or Illustrator export outlined the stroke into a filled shape, this is impossible and you have to re-export. That single fact drives half of §09.

2 · Sticky scrub — a diagram assembling itselfposition: sticky + scroll progress → transform & opacity only
↓ scroll inside
mean outlier · 3.1σ

Note what is being animated: transform and opacity only, never x/y/width/d. Attribute animation forces the browser through Style → Layout → Paint every frame; transform and opacity run on the compositor. This is the single biggest performance lever in SVG animation, and it's a discipline your artwork has to be built for — every bar sits in its own <g> with an explicit transform-box: fill-box.

3 · Morph — one diagram becoming anotherthis demo: matched point counts · dissimilar shapes need GSAP MorphSVG, 9.6 KB gz
↓ scroll inside
before →

Shape-to-shape transition is where the tools genuinely diverge. GSAP MorphSVG is the only option in this review that morphs between paths with different point counts — it re-samples and re-indexes automatically. Motion tells you to install Flubber; Anime.js requires identical point counts; CSS d interpolation requires identical command counts and Safari parses it but doesn't apply it. If your explainer says "this chart is really that chart," this one capability is the whole decision.

4 · Where SVG breaks — measure it yourselfanimated SVG nodes vs. frame rate, on your machine, right now
800 — fps

The peer-reviewed number to plan against comes from Schwab et al., Scalable Scalable Vector Graphics (IEEE TVCG 2022): ~2,500 animated SVG nodes lands around 30 fps, and 15,000–20,000 nodes collapses to ~6 fps. Because rendering and interaction share the main thread, 6 fps also means ~250 ms of input lag — a janky scroll explainer doesn't read as "slow," it reads as broken. Practical bands: under 1,000 nodes is safe everywhere including mid-tier mobile; 1,000–2,500 is fine on desktop and degrading on phones; above ~5,000 go to canvas; above ~50,000 go to WebGL. Your slider above is a desktop-class machine — divide by roughly three for a four-year-old Android.

The design implication: 40,000 animated dots is almost always a design failure rather than a rendering failure. Binning, aggregating and small-multiple-ing the data is cheaper, more legible and more accessible than switching to a GPU renderer to brute-force a chart nobody can read.

03Three tiers, one question

The fourteen tools in scope aren't really competing with each other — they occupy three different layers of the stack, and most "which should we use" arguments are people comparing across layers. Sorting them first makes the decision almost mechanical.

Tier 1 · Code-first runtimes

GSAP · Motion · Anime.js · native CSS

You write the animation. They animate whatever's already in the DOM — including SVG nodes, individually, with CSS still available. This is the only tier where the output stays accessible and text stays real.

Tier 2 · Design-tool runtimes

Rive · Lottie/LottieFiles · Jitter · Cavalry · Spline

A designer authors in a GUI; a runtime plays the file. You get authoring speed and lose per-node control. Weight ranges from 45 KB (lottie-light) to 6 MB+ (a real Spline scene).

Tier 3 · Rendering engines

Three.js · PixiJS · deck.gl

Not animation tools at all — they're renderers you drive with a Tier-1 library. You adopt one because SVG has run out of headroom or the story is genuinely 3D/geospatial, not because you want nicer motion.

Tier 0 · Not really tools

Typeflow / Algo

Typeflow is a free browser demo that renders MP4s and emails them to you. It's a beautiful proof that the Cavalry Web Player works in production — that's the real finding, and it belongs in Tier 2.

The single question that resolves the tier

Does a human need to read words inside the animation? If yes → Tier 1, SVG, non-negotiable. If no — an illustrated character, a product beauty shot, an abstract hero — Tier 2 or 3 is on the table, and should be code-split so it costs nothing for the 60% of visitors who bounce before reaching it.

04Comparison matrix

ToolTierScroll scrubWeight (gzip)Text / a11yReactSvelteCost floorVerdict
GSAP 3.151Best — pin, scrub, snap~48 KB typicalNative DOM@gsap/react (stale)Docs + skills, no pkg$0 commercialSpine
Native CSS timelines1Yes, off-thread0 KBNative DOMn/an/a$0Base layer
Motion 131Yes, but no pin~8–25 KBNative DOMFirst-party, RSC-awareCommunity only$0 (MIT)If React-only
Anime.js 4.51Yes, no pin~12–20 KBNative DOMNone officialNone official$0 (MIT)Lightest MIT
Rive2Yes — 3 ways384–881 KBCanvas/WASMExcellent hooksDIY (~20 lines)$9/seat/mo (annual; $17 monthly)One hero
Lottie (lottie-web)2goToAndStop45–74 KB + JSONSVG DOM, no semanticsCommunityCommunity$0 player · see §08Interchange
dotLottie2setFrame~507 KBRaster blitOfficialOfficial ✓sameOnly if heavy
Cavalry + Web Player2setFrame + live attrs~3 MB, betaGPU vectorDIYDIY$0 (free since Apr 2026)Watch
Jitter2n/a — exports Lottie————unverifiedAuthoring only
Spline2No true scrub API567 KB – 6 MB+3D rasterWrapper 13mo staleDead$120/seat/mo self-host ($150 monthly)Skip
Three.js r1853Via GSAP128–160 KBCanvasR3F 9 + dreiThrelte 8$0 (MIT)Code-split
PixiJS 8.193Via GSAP~148 KBCanvas@pixi/react 8Community$0 (MIT)Not this brief
deck.gl 9.33Time-based, not scrub174–297 KBCanvasLoose React peerNone$0 (MIT)Only if geo
Typeflow / Algo0n/a — emails MP4————free demoNot a product

Sizes are gzip −9, measured from published npm tarballs on 18 Aug 2026. Brotli in production runs roughly 15–25% lower. "Weight" for Tier 2 includes the WASM payload where applicable, which is where the surprises live.

The Tier-1 shortlist, in one paragraph each

GSAP 3.15 Recommended

Free for commercial use since Apr 2025 · custom Webflow licence, not OSI · ~48 KB gzip for core + ScrollTrigger + DrawSVG

It wins on three things nothing else has: ScrollTrigger's pin/scrub/snap (Motion and Anime both make you hand-roll sticky and neither offers snapping or nested horizontal scrollers), MorphSVG's mismatched-point-count morphing, and SVG transform normalisation — GSAP writes matrices into the transform attribute and gives you svgOrigin in user-space coordinates, which sidesteps the entire class of cross-browser transform-origin bugs. It's the heaviest option and it is not open source: the licence forbids using GSAP to build a competing visual animation builder. Agency client work is explicitly fine, and AI-generated GSAP code is explicitly fine. One caveat to know: ScrollTrigger contains zero references to ScrollTimeline — it's pure rAF, on the main thread, always.

Motion 13 Strong second

MIT · 13.1.0 published 10 Aug 2026 · scroll() quoted at 5.1 KB, hybrid animate ~18 KB

The best-engineered option and the only one that offloads scroll animations to the browser's native ScrollTimeline/ViewTimeline where supported, falling back to rAF automatically — so on Chrome, Edge and Safari 26 your scroll work runs off the main thread for free. Best-in-class reduced-motion handling (built in, not bolted on) and the only genuinely RSC-aware option via motion/react-client. The disqualifier for this brief: no pinning primitive, no snap, no containerAnimation, and morphing dissimilar paths means adding Flubber. It also has no official Svelte support — three community ports exist, none first-party, which is a real risk on client work.

Anime.js 4.5 Lightest MIT

MIT · 4.5.0 published 22 Jun 2026 · events/scroll module 7.8 KB, svg/drawable 1.5 KB

The v4 rewrite made it genuinely competitive: proper subpath exports, a real onScroll() with four sync modes including direct playback scrubbing and eased scroll, and a createScope() primitive with mediaQueries that handles both framework cleanup and reduced-motion. If you're in Svelte and want the smallest permissively-licensed footprint, this is the pick. No pin, no auto point-matching on morph, no official framework wrappers, and note that a 5.0.0-beta appeared on npm on 17 Aug 2026 with no published release notes — don't start a client project on it.

Native CSS scroll-driven animations Use as base layer

0 KB · ~85% global · Chrome/Edge 115+, Safari 26+ · Firefox stable (153) does not support it

Free, off the main thread, and immune to every jank cause in §07. It handles reveals, parallax, progress indicators, sticky-section scrubs, and — usefully — stroke-dashoffset, so basic path drawing works natively. It cannot pin with spacer management, cannot snap, cannot read progress into JS, and cannot morph paths cross-browser. Firefox has it behind layout.css.scroll-driven-animations.enabled, whose default value is literally @IS_NIGHTLY_BUILD@ — true in Nightly, false in Beta and Release. Bugzilla 1676779 is NEW with no target milestone and 32 open dependencies, so plan for 2027 at the earliest. Be careful reading caniuse here: its "Firefox 156" row is the Nightly track, not stable. Pitch it as the base layer and let it degrade to "no animation" in Firefox — that's a progressive-enhancement story, not a bug. The polyfill puts you back on the main thread and buys syntax, not performance.

05The rendering engines

You asked about physics/3D frameworks specifically, and given the explainer has spatial ambition this is where the money question sits: when is leaving the DOM justified?

Three.js r185 Code-split, for one or two moments

MIT · ~128 KB gzip for a minimal lit scene, ~160 KB realistic · WebGPU still experimental

Justified when: the camera genuinely moves through a spatial structure; you need >10k simultaneously animated marks; the geometry is volumetric, network or point-cloud; or a shader-driven transition between diagram states is the point. Overkill when: flowcharts, timelines, bar/line/Sankey, annotated schematics — anything with typography. Text is Three's weakest area (SDF-based, no rich text, no reflow, zero accessibility), and paying a 128 KB floor for a diagram SVG renders crisply for free is a bad trade.

Three things to know before quoting it. (1) @react-three/fiber@9.7 peers react: ">=19 <19.3" — an upper bound that will fight a Next.js upgrade; check the client's React version before you commit. (2) Three ships breaking changes under minor version bumps by policy — pin the exact version, never use a range. (3) drei's ScrollControls creates its own scroll container overlaying the canvas and does not compose with a long editorial article or position: sticky; for a text-heavy explainer use GSAP ScrollTrigger driving a plain render loop instead. In Svelte, Threlte 8 is thin, maintained and Svelte-5 native, with a genuinely useful <Svg> component (SVGLoader wrapper — a direct path from D3-generated geometry into 3D) and <MeshLine> for thick lines, which raw Three is bad at. Threlte has no ScrollControls equivalent; budget for hand-wiring.

On WebGPU: still officially experimental, not the default, and costs +78 KB while forfeiting ShaderMaterial, onBeforeCompile and EffectComposer. Firefox has it disabled by default. Ship WebGL.

PixiJS 8.19 Wrong tool for this brief

MIT · ~148 KB gzip · 2D GPU scenegraph

Excellent at what it's for — dense 2D fields, particle systems, continuous pan/zoom over tens of thousands of marks, at the same cost as Three but with real 2D semantics instead of hand-rolled ortho cameras. The disqualifier is specific and checkable: Pixi's SVG-to-geometry path does not support <text>, filters, or patterns, and its own docs warn that complex hole geometries "may render inaccurately" due to performance-optimised triangulation. A D3 chart is mostly text. "Render our D3 SVG through Pixi" silently drops all of it. Revisit only if a single scene genuinely exceeds ~10,000 animated 2D marks.

deck.gl 9.3 Only if the data is actually geospatial

MIT · 174 KB gzip minimum, ~297 KB with real geo layers

The right answer for genuine maps, >100k GPU-instanced marks with picking, H3/hex aggregation, or tiled data. Its GPU-interpolated attribute transitions (the transitions prop on get* accessors) are genuinely excellent at morphing between data states. Two things to flag: there is no official deck.gl scrollytelling documentation, example or recipe — the pattern is entirely community-assembled, and deck.gl's own /docs/roadmap page is stale enough to still discuss v6. And its view-state transitions are time-based interpolators, which fight scrub-based scrolling. The workable pattern is: use step enter events for camera moves, step progress only for cheap scrubbing like opacity and filter ranges. Pair with MapLibre (BSD-3, no token, no per-load billing) rather than Mapbox.

If the explainer's data isn't geographic, OrthographicView technically does infovis but you're paying 174 KB and inheriting a geo-shaped API for something D3 + SVG does better, more accessibly, at 31 KB.

Physics, specifically

Nothing in this list needs a physics engine, and adding one is the most common way a scroll explainer gets slow and unpredictable. Three tiers of "physics" worth distinguishing:

06The design-tool runtimes

Rive The one to actually buy

Runtimes MIT · @rive-app/canvas 2.40.0 · 384 KB (lite) / 813 KB (canvas) / 881 KB (webgl2) gzip incl. WASM

The only design-tool runtime built for interactive motion rather than playback. Three independent ways to drive it from scroll — rive.scrub() on a linear timeline, state-machine number inputs, or the modern data-binding path (viewModelInstance.number("Progress").value = p) — plus first-class React hooks. If the explainer needs a character, a product, or anything that responds rather than merely plays, this is the tool.

Two gotchas. Since 20 Oct 2025, exports are behind paid plans — you cannot ship a .riv on the free tier, so $9/seat/mo is the real floor — and note that's the annual rate; month-to-month is $17. And Rive's editor-side "Scroll Constraint" is internal scrolling within an artboard, not page-scroll binding; you wire IntersectionObserver or ScrollTrigger to a data-binding input yourself. No official Svelte wrapper (only an unofficial rivette at v0.0.2), but the core JS package is framework-agnostic and the Svelte integration is about twenty lines in an onMount.

Lottie Best as an interchange format, not a platform

lottie_light.min.js 45 KB gzip · @lottiefiles/dotlottie-web ~507 KB gzip

The single most proven scroll-scrub pattern on the web: anim.goToAndStop(frame, true) driven by ScrollTrigger, and GSAP ships an official LottieScrollTrigger helper for exactly this. The SVG renderer produces real DOM you can still target with CSS. At 45 KB it is unbeatable on weight.

The licensing landmine is worth reading carefully. LottieFiles' commercial-use guide defines commercial use as any activity using their services with intent of monetary gain, and states Free and Individual plans are non-commercial only — a startup marketing site needs the Team plan at $24.99/user/mo billed annually with a two-seat minimum — roughly $50/mo — and Team is itself capped at 10 users per company domain before a forced Enterprise upgrade. Critically, this is an authoring/service licence, not a runtime licence: the players themselves are MIT and carry no such restriction. So author your Lottie somewhere else — Cavalry (now free) or Jitter — and play it with the MIT player, and the licensing question evaporates.

Also flag: the old declarative scroll-interactivity library (@lottiefiles/lottie-interactivity) has been abandoned since Feb 2023, there is no dotlottie-interactivity package on npm, and every dotLottie state-machine method is annotated @experimental. For scroll, just call setFrame() yourself. dotLottie's Rust/WASM renderer is ~7× the wire weight of lottie-light for the same content and rasterises to a bitmap — only worth it for genuinely heavy scenes. It is, however, the only tool in this review with an official Svelte wrapper.

Cavalry The sleeper — two things changed in 2026

Free since Apr 2026 · Web Player in open beta, ~3 MB payload

Two developments most people haven't caught up with. (1) Canva acquired Cavalry in Feb 2026 and made it free in April — the full feature set, commercial use allowed, only Enterprise is paid. For a studio doing procedural, data-driven, spreadsheet-fed motion design, a free Cavalry is a significant capability, and it's the most natural authoring tool in this list for a dataviz practice. (2) There is now a Cavalry Web Player — a WASM runtime that plays native .cv scene files in the browser, with the full procedural engine intact rather than a baked export. Its scrub API is excellent: setFrame() plus live setAttribute(layerId, attrId, value), so you can drive any node attribute from scroll.

Why it's "watch" and not "use": ~3 MB compressed, still open beta, no npm package or framework wrappers, and — the blocker — the redistribution licence is unresolved. Canva's Cavalry Additional Terms don't mention the Web Player at all, while carrying a blanket "may not distribute the software" clause. Get that in writing before shipping it on a client site. In the meantime, the Cavalry → Lottie export path works, but note that deformers, duplicators, Magic Easing and rigs — exactly the procedural work you'd use Cavalry for — get baked frame-by-frame and inflate the JSON enormously.

Spline Skip for a scroll explainer

Runtime 567 KB gzip baseline · physics module +709 KB · realistic total 1.5–6 MB+

Three disqualifiers, any one of which would be enough. No timeline scrub API — the runtime exposes setVariable, emitEvent and findObjectByName, but there is no setFrame/seek; Spline's built-in Scroll Event is configured in the editor and its docs say it "only works with the Viewer export." Weight — 2–7× Rive before you count the scene file and textures. Cost — the free tier watermarks web exports, and self-hosted code export is gated to the Max tier at $120/seat/mo billed annually ($150 month-to-month); below that you're embedding Spline's hosted viewer and taking a third-party runtime dependency on a client's marketing site. The React wrapper is also 13 months stale against a runtime that shipped in June. It's a lovely tool for a one-off hero object; it is not a scroll-explainer engine.

Jitter & Typeflow Authoring speed / not a product

Jitter: Lottie JSON + video export only · Typeflow: renders MP4 and emails it to you

Jitter has no runtime of its own — it's a fast, Figma-native-feeling authoring front-end that exports Lottie JSON (no dotLottie) or video, so it collapses into the Lottie row. Its rebuilt Lottie exporter added full text-effect and mask support. Reasonable choice if a designer wants to move quickly without learning Cavalry. Its pricing page renders client-side and couldn't be verified — check it in a browser before budgeting. Typeflow is a free marketing demo by the studio Algo, explicitly described as the first public implementation of the Cavalry Web Player. Export is an MP4 by email. There's no SDK, no npm package, no self-serve licence; the commercial offer is an agency engagement. Its value to you is as evidence that the Cavalry Web Player works at production quality — route the interest there.

07Performance budget & the escalation ladder

Set the budget before choosing the tool, and most arguments resolve themselves. A marketing explainer that loses users to a 3-second LCP has failed regardless of how good the animation is.

Scene needsReach forMarginal cost
Reveal, parallax, progress barNative CSS animation-timeline0 KB, off main thread
Path draw, staggered build, pinned scrubGSAP + ScrollTrigger + DrawSVG~48 KB gzip, once
Chart A becomes chart B+ MorphSVG+9.6 KB
Object travels a generated route+ MotionPath+9.7 KB
Illustrated / stateful heroRive, dynamic import+384–813 KB, deferred
One genuine 3D momentThree.js WebGL, dynamic import+128–160 KB, deferred
>10k animated 2D marks in one scenePixiJS+148 KB
Actual geography, actual tilesdeck.gl + MapLibre+174–297 KB

Fix these before you change renderer — they're usually worth more than a rewrite

  1. Animate transform and opacity only. Changing x, cx, width or d mutates geometry and forces Layout + Paint every frame; transform and opacity run on the compositor. This is the single biggest lever, and per the TVCG paper the browser-side cost of that pipeline is "often greater than the time spent in the visualization's JavaScript code."
  2. Kill animated SVG filters. feGaussianBlur and feDropShadow are per-frame raster work, frequently not GPU-accelerated for SVG, and the number-one silent killer of mobile frame rate.
  3. Precompute path d strings at build or step time and swap rather than interpolate. Per-frame d rewrites are where most D3 scrollytelling dies.
  4. Cull off-screen scenes and use content-visibility: auto on steps that aren't visible.
  5. Measure p95/p99 frame time against the 16.7 ms budget, not mean fps. Mean fps hides exactly the stutter users notice.
Accessibility is not optional here, and it's cheap. prefers-reduced-motion should gate the whole timeline, not just soften it: gsap.matchMedia() with a reduce branch that renders the final state of every scene and creates no timeline at all. Motion has this built in via <MotionConfig reducedMotion="user">; GSAP and Anime.js both require you to write it. Do it once, in the scene component, and it's free forever after.

08What it costs

ToolFree tier realityCommercial floorLicence
GSAPEverything, including all former Club plugins$0Custom Webflow "standard" licence — not OSI. No building competing animation builders. Client work and AI-generated code explicitly OK.
MotionFull library, MIT$0MIT. Motion+ is optional (examples, components, editor); price not verifiable from the site, €299 one-time historically.
Anime.js / Three / Pixi / deck.gl / D3 / LenisEverything$0MIT / ISC / Apache
CavalryFull app, commercial use allowed$0Free since Apr 2026 (Canva). Web Player redistribution terms unresolved.
RiveNo exports on free tier$9/seat/mo billed annually · $17 month-to-monthRuntimes MIT, no royalties, all runtimes on every tier
LottieFilesNon-commercial only~$50/mo (Team, 2-seat min, 10-seat domain cap)Authoring service is restricted; players are MIT — author elsewhere and the fee disappears
SplineWatermarked web exports$120/seat/mo annual · $150 month-to-month, to self-host codeBelow Max you embed their hosted viewer
Jitter3 files, 720p, watermarkunverified — prices render client-side—

The commercially interesting finding: the recommended stack costs $0 in licensing, and the two paid tools worth considering are $9/mo (Rive) and $0 (Cavalry, free since April). The expensive options — Spline at $120–150/seat/mo, LottieFiles at ~$50/mo — are also the two least suited to this brief. That's a rare alignment, and it's worth putting in front of the startup explicitly: the technology recommendation and the budget recommendation point the same way.

09Figma / Illustrator → Claude → React & Svelte

The framework choice is maybe a third of the problem. The other two thirds is whether the SVG that comes out of your design tool is animatable at all, and whether Claude can work on it without drowning in path data. Both are solvable at the artwork stage and nearly unfixable afterward.

Four tracks, not one pipeline

TrackForAuthoringHandoffRuntime
A · UI motionComponent states, micro-interactions, page transitionsFigma Design + Figma MotionFigma MCP → get_design_context + get_motion_contextMotion or GSAP
B · Scroll explainerYour core deliverableIllustrator or Figma, disciplined layer namingClean SVG export → SVGO → scene filesGSAP ScrollTrigger + DrawSVG
C · Stateful heroCharacter, product, anything that respondsFigma → Copy as SVG → Rive.riv file + input names@rive-app/react-canvas
D · FallbackArcs, shaders, animated masks, boolean vectorsAnywherePre-rendered MP4/WebM, or Lottie<video> or lottie-light
The 2026 development that changes Track A: Figma shipped Figma Motion in open beta on 24 June 2026 — a real keyframe timeline on the canvas, with motion variables, animated components that travel across files, and a Dev Mode Motion tab that copies out CSS, JSON or motion.dev code. Its MCP server exposes get_motion_context, which returns pre-computed keyframe snippets keyed by the same data-node-id that get_design_context emits. That join is the whole handoff. But nothing found suggests Figma Motion models scroll-linked animation at all — it appears to be purely time-based. So it gives you scene keyframes; you still write the scrub. Track B remains hand-built, which is precisely why it's a differentiator.

Two export presets, permanently

These are mutually incompatible and it's worth saving them as named presets rather than rediscovering the conflict each time. Rive wants inline styles; the web/CSS path wants presentation attributes.

Preset "web" — for Track B

  • Figma: Include "id" attribute ON — this is the single most important setting; it maps layer names to SVG ids
  • Figma: Outline text OFF (keep live <text>)
  • Figma: Simplify stroke — verify the output; it can convert strokes to filled outlines, which makes path-drawing impossible
  • Illustrator: Export As (never Save As) · Styling: Presentation Attributes · Object IDs: Layer Names · Font: SVG · Decimal: 2 · Preserve Illustrator Editing: off
  • Keep viewBox, drop width/height

Preset "rive" — for Track C

  • Inline styles, not presentation attributes
  • Retain shape ids and names
  • Preserve Illustrator Editing Capabilities off
  • Unsupported on import and silently dropped: stroke-dasharray, masks, filters, skew transforms, gradient transforms; embedded images ignored
  • Lottie import is Enterprise-only

The five artwork rules that decide whether animation is possible

  1. One moving thing = one path. Figma collapses multiple shapes merged into a single vector into one d string, which cannot be animated per-part. Fix it at design time: duplicate the layer and delete points from each copy so each animatable element exports as its own path.
  2. Live strokes on anything that draws. fill="none", a real stroke attribute, open path. Never Outline Stroke a path you intend to draw. Variable-width brush strokes must be outlined — so those can never be drawn; use a mask reveal instead.
  3. Name for the selector you'll write. Kebab-case ASCII, no spaces, no leading digits: scene2__bar-01 … bar-12. Zero-padded indices mean one line of code ([id^="scene2__bar-"] with a stagger) instead of twelve.
  4. Group what moves together; flatten what never moves. Every extra <g> is a nested transform to reason about and tokens Claude has to read.
  5. Set transform-box: fill-box on anything that rotates or scales. SVG's transform origin defaults to the 0 0 of the whole canvas, not the shape — the classic bug where a rotating icon swings across the screen. transform-box: fill-box has been Baseline since 2020; GSAP additionally offers svgOrigin in user-space coordinates for elements that must share one pivot.

SVGO config that doesn't destroy your animation

SVGO's default preset will delete your viewBox and minify your ids to a, b, c — deterministically, which makes collisions between two optimised files more likely, not less.

// svgo.config.js — for animation assets
export default {
  multipass: true,
  plugins: [
    { name: 'preset-default', params: { overrides: {
      removeViewBox:        false,  // #1 cause of lost responsiveness
      cleanupIds:           false,  // your ids ARE the animation API
      collapseGroups:       false,  // groups are your animation wrappers
      mergePaths:           false,  // never merge separately-animated paths
      convertShapeToPath:   false,  // keep rect/circle for x/y/r animation
      removeHiddenElems:    false,  // layers that start hidden then animate in
      moveElemsAttrsToGroup:false,
      convertPathData:      { floatPrecision: 2 },
      removeUnknownsAndDefaults: { keepDataAttrs: true },
    }}},
    { name: 'removeDimensions' },   // drop width/height, keep viewBox
    { name: 'prefixIds' },          // gradient/clip id collision guard
    // reusePaths: OFF — turns paths into <use>, which can't be animated separately
  ],
};
The id-collision bug you will hit exactly once and then never forget. Illustrator emits linearGradient id="SVGID_1_" in every file; Figma emits clip0_1_2, paint0_linear_1_2. Inline two SVGs on one page and the first definition wins for both — one graphic silently steals the other's gradient or clip path. Fix with SVGO's prefixIds at build time, or with a per-instance id suffix from React's useId().

The part that actually matters for working with Claude

The dominant failure mode isn't the animation code — it's token cost. A detailed illustration or map can be 50k–200k tokens of pure path data. It floods the context window, and worse, Claude cannot tell which sub-curve is which limb. Nothing recovers this after the fact. Three habits fix it permanently:

  1. Keep the timeline in a separate file from the SVG markup. scene-03.svg and scene-03.motion.ts. Claude edits the timeline and never loads a single path. This is the single biggest token win available and it costs nothing.
  2. Split into one file per beat — scene-01.svg … scene-06.svg, all sharing one viewBox so coordinates stay comparable across scenes. Halves the context per edit and makes review tractable.
  3. Ship a MOTION.md next to the scenes listing the id inventory and the intended beat for each. Cheap to write, and it makes every subsequent Claude run consistent rather than re-derived. When you do need Claude to reason about structure, hand it a skeleton — grep -o 'id="[^"]*"' — not the geometry.

The component shape to standardise on

<!-- Scene.svelte — the pattern; React is the same with useGSAP() -->
<script>
  import { onMount } from 'svelte';
  import gsap from 'gsap';
  import ScrollTrigger from 'gsap/ScrollTrigger';
  gsap.registerPlugin(ScrollTrigger);

  let root, stage;
  onMount(() => {
    const mm = gsap.matchMedia();
    mm.add('(prefers-reduced-motion: no-preference)', () => {
      const tl = gsap.timeline({ scrollTrigger: {
        trigger: root, start: 'top top', end: 'bottom bottom',
        scrub: 0.5,                 // scrub IS the explainer — never toggleActions
        invalidateOnRefresh: true,
      }});
      tl.from('#scene1__arrow', { drawSVG: '0%' })
        .from('[id^="scene1__bar-"]', { scaleY: 0, transformOrigin: 'bottom', stagger: .05 })
        .to('#scene1__callout', { autoAlpha: 1 }, '<');
      return () => tl.scrollTrigger?.kill();   // HMR + StrictMode leak guard
    });
    // reduced-motion branch: render the final state, create no timeline at all
    return () => mm.revert();
  });
</script>

<section bind:this={root} class="scroll-root">
  <div bind:this={stage} class="stage"><Scene1 /></div>
  <div class="steps"><!-- text steps establish the scroll length --></div>
</section>

<style>
  .stage { position: sticky; top: 0; height: 100svh; }  /* svh, not vh — mobile URL bar */
</style>

Four structural rules embedded in that snippet, each of which prevents a specific recurring bug: prefer CSS position: sticky over ScrollTrigger's pin: true where you can, because pin rewrites the DOM by injecting a pin-spacer and that surprises both agents and layout assumptions; use 100svh not 100vh, or mobile URL-bar resize will retrigger everything; one timeline per scene component, composed by the page; and always kill on unmount, because React StrictMode's double-mount and Svelte HMR both leak ScrollTriggers otherwise.

Getting the SVG into the component

Two Figma MCP details worth knowing before you rely on it

Where D3 sits in all this

Worth being explicit since it's the house tool: in this architecture D3 is the geometry generator, not the animator. d3-shape, d3-scale, d3-hierarchy, d3-geo, d3-sankey emit the d strings and coordinates; GSAP moves them; ScrollTrigger decides when. Import submodules, never the bundle — the modular subset is 31 KB gzip against 90 KB for full d3.min.js. And for morph transitions, generate both end-state paths with D3 at build time and hand the pair to MorphSVG, rather than interpolating d per frame at runtime.

10Risk register & open items

ItemRiskWhat to do
ScrollamaNo npm publish since Jun 2022 — 4 years — despite being the de-facto standardUse @zeitonline/scrolleo (maintained, near-identical API), or accept it knowingly: it's a thin IntersectionObserver wrapper and IO hasn't changed
@gsap/reactNo release since Jan 2025. Unclear if "done" or unmaintainedLow risk — it's a small useLayoutEffect wrapper. Note the risk in the SOW; the core library is actively released (3.15.0, Apr 2026)
R3F React pinreact: ">=19 <19.3" — an upper bound that will block a React upgradeConfirm the startup's React version before quoting any Three.js work
Three.js versioningBreaking changes ship under minor bumps, by stated policyPin the exact version. Never a range.
Cavalry Web PlayerRedistribution licence genuinely unresolved; Canva's terms don't mention it and carry a blanket no-distribution clauseGet written confirmation from Cavalry/Canva before shipping. Until then, Cavalry → Lottie only
Firefox + CSS scroll timelinesStable is 153 and does not support it; caniuse's "156" row is the Nightly track and is easy to misread as shippedTreat native CSS as progressive enhancement. Don't polyfill — it returns you to the main thread and buys syntax, not performance
Anime.js v55.0.0-beta.2 on npm (17 Aug 2026), no release notes anywhereShip v4.5. Watch v5.
Motion + SvelteNo first-party Svelte support; three competing community portsIf the build is Svelte, this alone is a reason to pick GSAP or Anime.js instead
Figma MotionOpen beta; Figma's own docs warn features may change and performance may suffer. Plan gating is stated inconsistently across their blog and help docsFine for exploration; don't make a client deadline depend on it. Check gating on your own account
Jitter pricingCould not be verified — the pricing page renders client-side and the only third-party figure has mismatched tier namesCheck in a browser before including in any budget

What I'd do in the first week

  1. Build one scene end-to-end — Illustrator artwork → "web" export preset → SVGO → scene-01.svg + scene-01.motion.ts → GSAP scrub in the actual framework. One scene proves the whole pipeline and surfaces every export pathology at once.
  2. Decide React vs Svelte on the animation evidence, not just the app. React gets first-party Motion, R3F/drei and Rive hooks; Svelte gets a thinner Three wrapper (Threlte), better native ergonomics for hand-rolled scrollytelling, and the only official dotLottie wrapper — but no official Motion or Rive binding. If the answer is Svelte, GSAP becomes near-mandatory rather than merely recommended.
  3. Set the performance budget before the storyboard, so "can this scene be 8,000 dots" is answered by a number rather than an argument.
  4. Write the MOTION.md id convention before any artwork is drawn. It's ten minutes and it's the difference between Claude writing your timelines and Claude drowning in path data.
← Shader Playground