Skip to content

[BUG] Missing null check in DocumentProjectionNode #3348

@bk-tho

Description

@bk-tho

7. Environment details

Arc: Version 1.107.0 (66519)
Chromium Engine Version 139.0.7258.67
macOS 15.5
framer-motion: 11.18.2 (I have since upgraded to current motion@12.23.12 but the affected code seems to be the same between these versions)

2. Describe the bug

In our software, there are super rare cases where this error occurs: TypeError: Cannot read properties of null (reading 'scrollLeft'). It's this line in DocumentProjectionNode, document.body.scrollLeft to be exact:

x: document.documentElement.scrollLeft || document.body.scrollLeft,

The error occurs during rapid scrolling/zooming of a large scrollable area with loads of DOM elements (it's something like a Gantt-Chart).

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

I get it that you won't accept bug reports without reproduction. Since it only occurs very rarely, I unfortunately did not manage to reliably reproduce this.

I'm not exactly sure how document.body can even be null in the first place. The error occurs well after the application has loaded. I'm certain that it cannot be some code that runs before there is a body.

This is what Claude Sonnet 4 has to say, so take it with a grain of salt. But it seems vaguely plausible:

DOM Manipulation Edge Cases
With lots of DOM operations (which you mentioned), these scenarios become possible:

  • The element gets temporarily removed via document.body.remove() or similar
  • Document fragments or aggressive DOM manipulation temporarily detaches the body
  • Race conditions where one part of code removes/replaces the body while another tries to access it

Browser-Specific Edge Cases
During rapid scrolling/zooming operations:

  • Some browsers might temporarily detach elements during heavy reflows/repaints
  • Memory pressure from lots of DOM elements could cause temporary cleanup
  • Browser optimization during intensive operations might cause brief DOM inconsistencies

It must be triggered by one of these since those are the only occurences of motion in this code path. But I'm not sure which exactly (left out some attributes like classNames and stuff that most likely does not matter).

<motion.button
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{ opacity: 0, transition: { delay: 0 } }}
  transition={{ delay: 1 }}
  type="button"
>
 { /* … */ }
</motion.button>
<AnimatePresence initial={false}>
  {deferredOpen && (
    <motion.div
      initial={{ height: 0, overflow: 'clip' }}
      exit={{ height: 0, overflow: 'clip' }}
      animate={{ height: 'auto', overflow: 'visible' }}
      transition={{ duration: 0.2 }}
      onAnimationStart={() => {
        itemRef.current?.setAttribute('style', 'overflow: clip');
      }}
      onAnimationComplete={() => {
        if (open) {
          itemRef.current?.removeAttribute('style');
        }
      }}
    >
       { /* … */ }
    </motion.div>
  )}
</AnimatePresence>
 <motion.div
  animate={{
    backgroundColor: highlighted
      ? // notice-4
        '#FFE3016B'
      : '#FFFFFF00',
    transition: {
      delay: highlighted ? 0.6 : 0,
      duration: 0.3,
      ease: 'easeInOut',
    },
  }}
  initial={false}
  onAnimationComplete={() => {
    if (highlighted) {
      highlightContext.setHighlightedEventId(null);
    }
  }}
/>

4. Steps to reproduce

  • In our application, scroll a lot

5. Expected behavior

This error should not occur.

This would be a fix of course. Not sure if there's a better solution. Possibly wrapping it in requestAnimationFrame?

import { addDomEvent } from "../../events/add-dom-event"
import { createProjectionNode } from "./create-projection-node"

export const DocumentProjectionNode = createProjectionNode<Window>({
  attachResizeListener: (
    ref: Window | Element,
    notify: VoidFunction,
  ): VoidFunction => addDomEvent(ref, 'resize', notify),
  measureScroll: () => ({
    x: (document.documentElement.scrollLeft || document.body?.scrollLeft) ?? 0,
    y: (document.documentElement.scrollTop || document.body?.scrollTop) ?? 0,
  }),
  checkIsScrollRoot: () => true,
});

6. Video or screenshots

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions