Disruptive content "jumps" during collaboration

Problem:

I have a collaborative editor using ProseMirror and YJS. When a peer (editor in a different tab) adds content to the top of the document, a user will experience, their content shifting lower in their viewport. I’ve noticed that this occurs even in the example demo code that is on the YJS Docs here as well as this Tiptap prosemirror collab scroll sandbox regardless of which browser I choose (firefox, chrome, safari).

Anyone have any insight or paths to what I should look into next?

screenshot:

jumping content yjs doc example


Research:

What I’ve tried:

  • Tried removing all custom extensions and plugins and tested the basic editor.
  • A couple changes I made to allow anchoring to WORK in firefox. (but not any other browser)
    • added a unique id to each paragraph/header node to help browser identify elements during rerender.
    • added overflow-anchor: none to all surrounding elements that are not part of the ProseMirror editor.
  • Confirmed that Chrome will set its scroll-anchor to the ProseMirror editor and not it’s child elements
    • I believe this is due to the w3c spec for choosing priority candidates during the anchor selection
      • chosen as anchor since contenteditable = true
1 Like

If you can reproduce this without Tiptap and YJS, using proseMirror-collab or just plain ProseMirror primitives, I can debug it. I set up a test where I just had a timeout insert content at the top of the document, but the scroll position stayed stable there in both Chrome and Firefox.

1 Like

I’m unfortunately seeing the same issue, stripped everything back and the only thing that makes a difference is removing “contenteditable=true”, when you do that the scroll anchoring immediately works correctly but obviously that’s not a solution.

Really seems related to the browsers logic for choosing the correct anchor

I have filed an issue in chromium here for anyone interested you could star to get more attention on this issue that affects all instances of ProseMirror:

https://issues.chromium.org/issues/395489580

1 Like

If you’re following along the chromium issue was fixed in Canary and is currently targeted to Chrome 135

That’s great. Thanks for filing that bug.

Oof, it seems like this is still an issue in both Firefox and Safari. I guess I can try to open similar issues against those projects. Are folks doing anything as workarounds in the mean time? This is a pretty rough experience.

For reproduction, you don’t even need prosemirror-collab. Here’s a plugin that will trigger it:

  new Plugin({
    view(view) {
      let intervalId = null;
      const button = document.createElement("button");
      button.type = "button";
      button.appendChild(document.createTextNode("Start typing"));
      button.addEventListener("click", () => {
        if (intervalId) {
          clearInterval(intervalId);
          button.innerText = "Start typing";
        } else {
          intervalId = setInterval(() => {
            view.dispatch(view.state.tr.insertText("a", 1));
          });
          button.innerText = "Stop typing";
        }
      });
      view.dom.parentElement.prepend(button);

      return {
        destroy() {
          if (intervalId) {
            clearInterval(intervalId);
          }
          button.remove();
        },
      };
    },
  }),

You have to scroll down in the document, and then you have to focus the document. Once your cursor is in the document below where text is being auto-inserted, your cursor will get pushed down one line every time the plugin types one line worth of content. Basically, it seems that when there is an active selection, the scroll becomes pinned to the top of the document, rather than to the viewport (or something, I didn’t describe that well).

With that plugin, in both Firefox and Chrome Linux, my visible content seems to remain entirely stable when I have the cursor at the bottom of the page with the insertion position scrolled out of view.

Could you say a bit more about what browser you’re testing with? And how your editor handles scrolling general (full page, editor element, some wrapper)?

I’m using Firefox on Linux. I made a little demo editor, here’s the full HTML and CSS (I’m mounting the EditorView on the root element):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ProseMirror Demo</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/demo/main.tsx"></script>
  </body>
</html>
.ProseMirror {
  border: thin solid black;
  border-radius: 0.25rem;
  padding: 1rem;
  outline: none;
  min-height: 200px;
}

#root {
  margin: auto;
  width: 80%;
  max-width: 700px;
}

So, it’s the body that’s actually scrolling as the editor grows.

Here’s a little video of the issue in case that’s useful: Screencast From 2025-09-10 13-07-57.mp4 - Nextcloud (I know the tab says “React ProseMirror Demo”, but that’s just because I reused that demo setup — this is with plain prosemirror-view, no React)

Originally we found this in a much more complex editor, using WebKit, on both Linux and macOS (via Tauri, a desktop app framework). I’ll take a look at that and see what element is scrolled there, too

In case it’s relevant, I am seeing the same behavior if I fix the height of the EditorView’s dom, such that only its content scrolls, e.g.

.ProseMirror {
  border: thin solid black;
  border-radius: 0.25rem;
  padding: 1rem;
  outline: none;
  min-height: 200px;
  overflow-y: auto;
  height: 80dvh;
}

#root {
  margin: auto;
  width: 80%;
  max-width: 700px;
}

Okay, I can reproduce this now. It seems to be an issue in Firefox’s native scroll anchoring. It has been present at least since version 121 so not a recent regression I guess. You can reproduce it with this simple page as well (note view is stable until you focus the editable content):

<div contenteditable=true></div>

<script>
  let d = document.querySelector("div")
  for (let i = 1; i < 100; i++) {
    d.appendChild(document.createElement("p")).textContent = "Paragraph " + i
  }
  window.scrollTo(0, 1e6)
  setInterval(() => {
    d.insertBefore(document.createElement("p"), d.firstChild).textContent = "New paragraph"
  }, 500)
</script>

We could do our own custom scroll anchoring, I guess, but that’s quite a messy and error-prone thing to implement, and super disruptive when it has bugs.

I’ve filed a Firefox issue for the time being.

1 Like