Describe the bug
On iOS Safari, a motion.div animating opacity + y via whileInView shows a one-frame flash at its underlying inline opacity: 0 at the moment the animation completes — the fully-revealed element visibly vanishes for an instant and reappears. Desktop browsers are unaffected.
Mechanism (from reading motion-dom 12.38.0): opacity is in acceleratedValues, so it runs as a native WAAPI animation, while y runs on the JS frameloop. The element's inline style still carries the pre-animation opacity: 0 (SSR-emitted; never overwritten while WAAPI composites on top). At completion, the finish handoff does setStyle(..., opacity: 1) followed by animation.cancel(). On WebKit the style commit and the cancel can land in different frames — for one frame the canceled animation no longer composites but the committed style isn't painted yet, so the element paints at the underlying inline opacity: 0.
Environment
- framer-motion
12.38.0 (motion-dom 12.38.0), Next.js 16 App Router (SSR emits the initial hidden style inline)
- Reproduces: iPhone, iOS Safari (owner-observed consistently across ~25
whileInView instances on one page; each element flashes once, right after its own reveal completes)
- Not reproducible: desktop Chrome/Chromium (same page)
Repro sketch
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "0px 0px -50px 0px" }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
{content}
</motion.div>
Scroll elements into view on an iPhone; watch each block flash once immediately after its reveal settles. (SSR/inline style="opacity:0" at first paint appears to be a precondition — the underlying value the flash reveals.)
Evidence trail
- Verified the WAAPI routing at runtime: during reveal,
document.getAnimations() shows a native Animation with opacity keyframes targeting the element; completed elements rest at inline opacity: 1; transform: none.
- Workaround confirmed on device: passing any
onUpdate to the component (supportsBrowserAnimation() gates on !onUpdate, waapi.mjs) forces opacity onto the JS path — the flash disappears completely with no other change.
- Compositor-side mitigations (
will-change: opacity, backface-visibility: hidden on the animated node) do NOT fix it — tested on device, both failed, which fits the handoff-ordering explanation rather than layer teardown.
Suggested direction
Commit the final keyframe style and flush/await the paint before cancel() on WebKit — or use commitStyles()-style ordering so there is no frame where neither the animation nor the committed style holds the final opacity. (Noted the WAAPI-commit hardening in 12.34.5/12.35.x changelogs; 12.38.0 still reproduces.)
Happy to provide more detail; I can't capture iOS screen recordings from this environment, but the on-device observation protocol above reproduces it reliably.
Describe the bug
On iOS Safari, a
motion.divanimatingopacity+yviawhileInViewshows a one-frame flash at its underlying inlineopacity: 0at the moment the animation completes — the fully-revealed element visibly vanishes for an instant and reappears. Desktop browsers are unaffected.Mechanism (from reading
motion-dom12.38.0):opacityis inacceleratedValues, so it runs as a native WAAPI animation, whileyruns on the JS frameloop. The element's inline style still carries the pre-animationopacity: 0(SSR-emitted; never overwritten while WAAPI composites on top). At completion, the finish handoff doessetStyle(..., opacity: 1)followed byanimation.cancel(). On WebKit the style commit and the cancel can land in different frames — for one frame the canceled animation no longer composites but the committed style isn't painted yet, so the element paints at the underlying inlineopacity: 0.Environment
12.38.0(motion-dom12.38.0), Next.js 16 App Router (SSR emits the initial hidden style inline)whileInViewinstances on one page; each element flashes once, right after its own reveal completes)Repro sketch
Scroll elements into view on an iPhone; watch each block flash once immediately after its reveal settles. (SSR/inline
style="opacity:0"at first paint appears to be a precondition — the underlying value the flash reveals.)Evidence trail
document.getAnimations()shows a nativeAnimationwith opacity keyframes targeting the element; completed elements rest at inlineopacity: 1; transform: none.onUpdateto the component (supportsBrowserAnimation()gates on!onUpdate,waapi.mjs) forces opacity onto the JS path — the flash disappears completely with no other change.will-change: opacity,backface-visibility: hiddenon the animated node) do NOT fix it — tested on device, both failed, which fits the handoff-ordering explanation rather than layer teardown.Suggested direction
Commit the final keyframe style and flush/await the paint before
cancel()on WebKit — or usecommitStyles()-style ordering so there is no frame where neither the animation nor the committed style holds the final opacity. (Noted the WAAPI-commit hardening in 12.34.5/12.35.x changelogs; 12.38.0 still reproduces.)Happy to provide more detail; I can't capture iOS screen recordings from this environment, but the on-device observation protocol above reproduces it reliably.