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
~60 KB gzip total. Real text, real accessibility, SEO-visible, screenshot-able, print-able. Pin, scrub and snap are only available here.
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.
Scrollama is still the standard but hasn't published to npm since June 2022. Scrolleo is the maintained near-clone.
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.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.
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.
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.
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.
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.
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
| Tool | Tier | Scroll scrub | Weight (gzip) | Text / a11y | React | Svelte | Cost floor | Verdict |
|---|---|---|---|---|---|---|---|---|
| GSAP 3.15 | 1 | Best — pin, scrub, snap | ~48 KB typical | Native DOM | @gsap/react (stale) | Docs + skills, no pkg | $0 commercial | Spine |
| Native CSS timelines | 1 | Yes, off-thread | 0 KB | Native DOM | n/a | n/a | $0 | Base layer |
| Motion 13 | 1 | Yes, but no pin | ~8–25 KB | Native DOM | First-party, RSC-aware | Community only | $0 (MIT) | If React-only |
| Anime.js 4.5 | 1 | Yes, no pin | ~12–20 KB | Native DOM | None official | None official | $0 (MIT) | Lightest MIT |
| Rive | 2 | Yes — 3 ways | 384–881 KB | Canvas/WASM | Excellent hooks | DIY (~20 lines) | $9/seat/mo (annual; $17 monthly) | One hero |
| Lottie (lottie-web) | 2 | goToAndStop | 45–74 KB + JSON | SVG DOM, no semantics | Community | Community | $0 player · see §08 | Interchange |
| dotLottie | 2 | setFrame | ~507 KB | Raster blit | Official | Official ✓ | same | Only if heavy |
| Cavalry + Web Player | 2 | setFrame + live attrs | ~3 MB, beta | GPU vector | DIY | DIY | $0 (free since Apr 2026) | Watch |
| Jitter | 2 | n/a — exports Lottie | — | — | — | — | unverified | Authoring only |
| Spline | 2 | No true scrub API | 567 KB – 6 MB+ | 3D raster | Wrapper 13mo stale | Dead | $120/seat/mo self-host ($150 monthly) | Skip |
| Three.js r185 | 3 | Via GSAP | 128–160 KB | Canvas | R3F 9 + drei | Threlte 8 | $0 (MIT) | Code-split |
| PixiJS 8.19 | 3 | Via GSAP | ~148 KB | Canvas | @pixi/react 8 | Community | $0 (MIT) | Not this brief |
| deck.gl 9.3 | 3 | Time-based, not scrub | 174–297 KB | Canvas | Loose React peer | None | $0 (MIT) | Only if geo |
| Typeflow / Algo | 0 | n/a — emails MP4 | — | — | — | — | free demo | Not 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:
- Spring easing — what you almost always actually want. Free in Motion (
type: "spring"), in Anime.js, in Svelte's built-inspringstore, and in Figma Motion's own easing set. Zero extra bytes. - Inertia / momentum — GSAP's
InertiaPlugin(now free, 4.3 KB) for flick-and-settle interactions. Worth it for a draggable exploration step; pointless on a scrubbed timeline. - Rigid-body simulation — Rapier (via
@react-three/rapier) or Spline's physics module. Note Spline's physics bundle alone is 709 KB gzip, larger than its already-huge runtime. Simulation is fundamentally at odds with scrubbing: you can play a simulation forward on scroll, but you cannot scrub it backwards deterministically without baking it to keyframes first. For an explainer, bake it.
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 needs | Reach for | Marginal cost |
|---|---|---|
| Reveal, parallax, progress bar | Native CSS animation-timeline | 0 KB, off main thread |
| Path draw, staggered build, pinned scrub | GSAP + 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 hero | Rive, dynamic import | +384–813 KB, deferred |
| One genuine 3D moment | Three.js WebGL, dynamic import | +128–160 KB, deferred |
| >10k animated 2D marks in one scene | PixiJS | +148 KB |
| Actual geography, actual tiles | deck.gl + MapLibre | +174–297 KB |
Fix these before you change renderer — they're usually worth more than a rewrite
- Animate
transformandopacityonly. Changingx,cx,widthordmutates 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." - Kill animated SVG filters.
feGaussianBlurandfeDropShadoware per-frame raster work, frequently not GPU-accelerated for SVG, and the number-one silent killer of mobile frame rate. - Precompute path
dstrings at build or step time and swap rather than interpolate. Per-framedrewrites are where most D3 scrollytelling dies. - Cull off-screen scenes and use
content-visibility: autoon steps that aren't visible. - Measure p95/p99 frame time against the 16.7 ms budget, not mean fps. Mean fps hides exactly the stutter users notice.
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
| Tool | Free tier reality | Commercial floor | Licence |
|---|---|---|---|
| GSAP | Everything, including all former Club plugins | $0 | Custom Webflow "standard" licence — not OSI. No building competing animation builders. Client work and AI-generated code explicitly OK. |
| Motion | Full library, MIT | $0 | MIT. Motion+ is optional (examples, components, editor); price not verifiable from the site, €299 one-time historically. |
| Anime.js / Three / Pixi / deck.gl / D3 / Lenis | Everything | $0 | MIT / ISC / Apache |
| Cavalry | Full app, commercial use allowed | $0 | Free since Apr 2026 (Canva). Web Player redistribution terms unresolved. |
| Rive | No exports on free tier | $9/seat/mo billed annually · $17 month-to-month | Runtimes MIT, no royalties, all runtimes on every tier |
| LottieFiles | Non-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 |
| Spline | Watermarked web exports | $120/seat/mo annual · $150 month-to-month, to self-host code | Below Max you embed their hosted viewer |
| Jitter | 3 files, 720p, watermark | unverified — 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
| Track | For | Authoring | Handoff | Runtime |
|---|---|---|---|---|
| A · UI motion | Component states, micro-interactions, page transitions | Figma Design + Figma Motion | Figma MCP → get_design_context + get_motion_context | Motion or GSAP |
| B · Scroll explainer | Your core deliverable | Illustrator or Figma, disciplined layer naming | Clean SVG export → SVGO → scene files | GSAP ScrollTrigger + DrawSVG |
| C · Stateful hero | Character, product, anything that responds | Figma → Copy as SVG → Rive | .riv file + input names | @rive-app/react-canvas |
| D · Fallback | Arcs, shaders, animated masks, boolean vectors | Anywhere | Pre-rendered MP4/WebM, or Lottie | <video> or lottie-light |
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" attributeON — this is the single most important setting; it maps layer names to SVG ids - Figma:
Outline textOFF (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, dropwidth/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
- One moving thing = one path. Figma collapses multiple shapes merged into a single vector into one
dstring, 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. - Live strokes on anything that draws.
fill="none", a realstrokeattribute, open path. NeverOutline Strokea path you intend to draw. Variable-width brush strokes must be outlined — so those can never be drawn; use a mask reveal instead. - 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. - Group what moves together; flatten what never moves. Every extra
<g>is a nested transform to reason about and tokens Claude has to read. - Set
transform-box: fill-boxon anything that rotates or scales. SVG's transform origin defaults to the0 0of the whole canvas, not the shape — the classic bug where a rotating icon swings across the screen.transform-box: fill-boxhas been Baseline since 2020; GSAP additionally offerssvgOriginin 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
],
};
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:
- Keep the timeline in a separate file from the SVG markup.
scene-03.svgandscene-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. - Split into one file per beat —
scene-01.svg…scene-06.svg, all sharing oneviewBoxso coordinates stay comparable across scenes. Halves the context per edit and makes review tractable. - Ship a
MOTION.mdnext 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
- Whole-element motion only (fade/slide/scale the graphic as a unit) → keep it as
<img src="scene.svg">and animate the wrapper. Zero token cost, zero inlining. Most scenes qualify for at least part of this. - Path geometry animates (draw, trim, morph, per-part) → must inline. React:
vite-plugin-svgrwithsvgrOptions: { ref: true }(you need the ref for GSAP) and an explicitsvgoConfig, or SVGR's default SVGO run strips your ids. Svelte:@poppanator/sveltekit-svg, or for a handful of hero scenes just paste the SVG into the.sveltefile — you getbind:this, reactive bindings and scoped-CSS id handling for free, and Claude sees the markup in the same file it's editing. - An icon used 50 times → sprite plus
<use>, with the caveat that two<use>of the same path can't be animated independently.
Two Figma MCP details worth knowing before you rely on it
- Exact
nodeIdmatch beatsfallbackNodeId. The fallback is the backing component, shared across every instance — matching on it applies your motion to all of them. - Interleaved transforms need offset maths. If a node has a static base transform (
rotate-45) and animated transforms,get_motion_contextreturns the absolute value. Animate the offset on the wrapper, or 45° at rest silently becomes 90°.
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
| Item | Risk | What to do |
|---|---|---|
| Scrollama | No npm publish since Jun 2022 — 4 years — despite being the de-facto standard | Use @zeitonline/scrolleo (maintained, near-identical API), or accept it knowingly: it's a thin IntersectionObserver wrapper and IO hasn't changed |
@gsap/react | No release since Jan 2025. Unclear if "done" or unmaintained | Low 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 pin | react: ">=19 <19.3" — an upper bound that will block a React upgrade | Confirm the startup's React version before quoting any Three.js work |
| Three.js versioning | Breaking changes ship under minor bumps, by stated policy | Pin the exact version. Never a range. |
| Cavalry Web Player | Redistribution licence genuinely unresolved; Canva's terms don't mention it and carry a blanket no-distribution clause | Get written confirmation from Cavalry/Canva before shipping. Until then, Cavalry → Lottie only |
| Firefox + CSS scroll timelines | Stable is 153 and does not support it; caniuse's "156" row is the Nightly track and is easy to misread as shipped | Treat native CSS as progressive enhancement. Don't polyfill — it returns you to the main thread and buys syntax, not performance |
| Anime.js v5 | 5.0.0-beta.2 on npm (17 Aug 2026), no release notes anywhere | Ship v4.5. Watch v5. |
| Motion + Svelte | No first-party Svelte support; three competing community ports | If the build is Svelte, this alone is a reason to pick GSAP or Anime.js instead |
| Figma Motion | Open beta; Figma's own docs warn features may change and performance may suffer. Plan gating is stated inconsistently across their blog and help docs | Fine for exploration; don't make a client deadline depend on it. Check gating on your own account |
| Jitter pricing | Could not be verified — the pricing page renders client-side and the only third-party figure has mismatched tier names | Check in a browser before including in any budget |
What I'd do in the first week
- 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. - 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.
- Set the performance budget before the storyboard, so "can this scene be 8,000 dots" is answered by a number rather than an argument.
- Write the
MOTION.mdid 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.