This one has a happier ending than most Framer export problems: scroll-reveal ("appear") animations are one of the few interactions Framer genuinely ships in a recoverable, static format — a lot of exports just fail to use it correctly.
Where the data actually lives
Framer's server-rendered HTML includes a <script type="framer/appear"> block per animated element, containing real authored animation data: initial state (usually opacity: 0.001 — a deliberate near-zero marker, not a bug), the target animate state, and a full transition spec — duration, easing curve, or spring parameters (mass, stiffness, damping/bounce). This is the actual data Framer's designer set when they configured the animation, not a runtime computation. It's sitting right there in the page source before any JavaScript runs.
Why exports still lose it
A few common failure modes we've run into:
- The export strips the appear script along with the rest of Framer's runtime, without first reading the data out of it — so the animation state (elements permanently stuck at
opacity: 0.001) survives, but nothing ever triggers the transition to full opacity. Content silently vanishes. - A second, independent fetch for animation data doesn't match the fetch used for the page HTML. Framer's appear-id hashes need to come from the exact same response as the DOM they're mapped to — fetching the animation data separately risks a mismatch that silently produces zero working animations.
- The reveal trigger (scroll into view) isn't reimplemented at all, so even correctly-extracted data has nothing to fire it.
What a correct implementation looks like
- Parse the
framer/appearscript(s) from the same HTML response used to build the rest of the page — never a second, separate request. - Map each element's
data-framer-appear-idto its initial/animate/transition spec. - Reproduce it with a real animation library that supports spring physics (not just CSS cubic-bezier approximations) — Framer Motion is the natural fit since it's the same physics engine family.
- Trigger on scroll-into-view with an IntersectionObserver, matching Framer's own
once/threshold behavior. - Add a defensive sweep for edge cases IntersectionObserver can legitimately miss — elements nested inside
position: stickyancestors are a known one, where the observer's relationship with the pinned ancestor doesn't always fire the way a plain scrolling element would.
How to tell if your export got this right
Inspect an element that should fade/slide in on scroll. If its inline style still shows opacity: 0.001 after you've scrolled it into view and waited a couple seconds, the extraction step is either missing or mismatched — that's permanently hidden content, not a subtle bug.