-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Current Workaround (CSS-only)
We are currently forced to use a plain HTML <div> with CSS transitions instead of Framer Motion:
<div
style={{
opacity: isSettled && isVisible ? 1 : 0,
transition: isSettled ? 'opacity 0.3s ease-in-out' : 'none'
}}
/>How this works:
-
transition: 'none'disables interpolation:
Settingtransitionto'none'completely disables the browser's interpolation engine. Whenopacitychanges during this time, the browser applies the new value instantly—no tweening, no easing. -
isSettled && isVisibleforces hidden state during init:
During initialization (isSettled === false), we forceopacityto0regardless of what the measurement system reports, preventing any visual appearance during the unstable period. -
The handoff:
WhenisSettledflips totrue,transitionenables and the element snaps instantly to the actualisVisiblestate. Only subsequent changes animate smoothly.
Concrete Example: The IntersectionObserver Flutter
A sticky date header fades based on scroll position calculated via an IntersectionObserver. Here is the timeline of what happens during initialization:
The measurement chaos (first 100ms):
- T=0ms: Observer not initialized →
isVisible: false,isSettled: false - T=20ms: Observer fires first measurement →
isVisible: true(element thinks it's visible) - T=40ms: Measurement refines/corrects →
isVisible: false(actually it's hidden) - T=60ms: Layout shifts slightly →
isVisible: true(correction again)
With CSS workaround (working):
T=0ms: isSettled=false, isVisible=false
opacity: 0 (forced), transition: none → Renders invisible instantly
T=20ms: isSettled=false, isVisible=true
opacity: 0 (forced by && logic), transition: none → Still invisible, no animation triggered
T=40ms: isSettled=false, isVisible=false
opacity: 0, transition: none → No change
T=100ms: isSettled=true (settling complete), isVisible=false
opacity: 0, transition: opacity 0.3s → Snaps to correct state instantly (no fade because no state change)
[Later when user scrolls]: isVisible becomes true
opacity animates 0→1 over 0.3s → Smooth fade in
Why Existing Framer Motion APIs Are Insufficient
initial={false}: Only suppresses the mount animation (T=0ms). At T=20ms, T=40ms, and T=60ms, every state change triggers a new animation.- Dynamic
transition: { duration: 0 }: WhenisSettledflips totrue, ifisVisibleistrue, Framer Motion animates from0(forced value) to1over 0.3s, triggering the exact fade-in we wanted to suppress. transition.delay: Only delays the animation start time. IfisVisibleflips 5 times during a 150ms delay, the final animation still executes from whatever intermediate value the element was left at.AnimatePresence: Designed for mount/unmount transitions. Using it for prop-level flutter causes DOM thrashing, which triggers more IntersectionObserver updates and worsens the flicker.
The Core Architectural Limitation
CSS allows removing the transition mechanism entirely (transition: none) during unstable periods. Framer Motion's animation engine is always active once a component mounts. There is no declarative API to say: "Disable all interpolation for 150ms, snap instantly to values, then enable smooth transitions only for future changes."
Proposed API
Option A: settled boolean prop
<motion.div
animate={{ opacity: isVisible ? 1 : 0 }}
transition={{ duration: 0.3 }}
settled={isSettled} // When false: CSS-equivalent of 'transition: none'
/>When settled flips from false → true, the current animate value applies instantly (no catch-up animation), and only subsequent changes respect the transition definition.
Option B: settleDuration number prop
<motion.div
animate={{ opacity: isVisible ? 1 : 0 }}
transition={{ duration: 0.3 }}
settleDuration={150} // First 150ms: CSS-equivalent of 'transition: none'
/>Use Cases
- Scroll-driven header visibility (IntersectionObserver initialization)
- Dynamic layout calculations (reading element bounds before animating)
- Virtual list render passes (position stabilizes after initial measurement)
- Any async measurement that may refine its initial guess shortly after mount