Skip to content

[BUG] iOS Safari: one-frame flash of underlying opacity at WAAPI animation completion (whileInView opacity + transform) #3778

Description

@mgoddyncsa

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions