diff --git a/packages/core/src/api/positionMapping.test.ts b/packages/core/src/api/positionMapping.test.ts
new file mode 100644
index 000000000..6b45e1da2
--- /dev/null
+++ b/packages/core/src/api/positionMapping.test.ts
@@ -0,0 +1,370 @@
+import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
+import * as Y from "yjs";
+import { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
+import { trackPosition } from "./positionMapping.js";
+
+describe("PositionStorage with local editor", () => {
+  let editor: BlockNoteEditor;
+
+  beforeEach(() => {
+    editor = BlockNoteEditor.create();
+    editor.mount(document.createElement("div"));
+  });
+
+  afterEach(() => {
+    editor.mount(undefined);
+    editor._tiptapEditor.destroy();
+  });
+
+  describe("mount and unmount", () => {
+    it("should register transaction handler on creation", () => {
+      editor._tiptapEditor.on = vi.fn();
+      trackPosition(editor, 0);
+
+      expect(editor._tiptapEditor.on).toHaveBeenCalledWith(
+        "transaction",
+        expect.any(Function)
+      );
+    });
+  });
+
+  describe("set and get positions", () => {
+    it("should store and retrieve positions without Y.js", () => {
+      const getPos = trackPosition(editor, 10);
+
+      expect(getPos()).toBe(10);
+    });
+
+    it("should handle right side positions", () => {
+      const getPos = trackPosition(editor, 10, "right");
+
+      expect(getPos()).toBe(10);
+    });
+  });
+
+  it("should update mapping for local transactions before the position", () => {
+    // Set initial content
+    editor.insertBlocks(
+      [
+        {
+          id: "1",
+          type: "paragraph",
+          content: [
+            {
+              type: "text",
+              text: "Hello World",
+              styles: {},
+            },
+          ],
+        },
+      ],
+      editor.document[0],
+      "before"
+    );
+
+    // Start tracking
+    const getPos = trackPosition(editor, 10);
+
+    // Move the cursor to the start of the document
+    editor.setTextCursorPosition(editor.document[0], "start");
+
+    // Insert text at the start of the document
+    editor.insertInlineContent([
+      {
+        type: "text",
+        text: "Test",
+        styles: {},
+      },
+    ]);
+
+    // Position should be updated according to mapping
+    expect(getPos()).toBe(14);
+  });
+
+  it("should not update mapping for local transactions after the position", () => {
+    // Set initial content
+    editor.insertBlocks(
+      [
+        {
+          id: "1",
+          type: "paragraph",
+          content: [
+            {
+              type: "text",
+              text: "Hello World",
+              styles: {},
+            },
+          ],
+        },
+      ],
+      editor.document[0],
+      "before"
+    );
+    // Start tracking
+    const getPos = trackPosition(editor, 10);
+
+    // Move the cursor to the end of the document
+    editor.setTextCursorPosition(editor.document[0], "end");
+
+    // Insert text at the end of the document
+    editor.insertInlineContent([
+      {
+        type: "text",
+        text: "Test",
+        styles: {},
+      },
+    ]);
+
+    // Position should not be updated
+    expect(getPos()).toBe(10);
+  });
+
+  it("should track positions on each side", () => {
+    editor.replaceBlocks(editor.document, [
+      {
+        type: "paragraph",
+        content: "Hello World",
+      },
+    ]);
+
+    // Store position at "Hello| World"
+    const getCursorPos = trackPosition(editor, 6);
+    const getStartPos = trackPosition(editor, 3);
+    const getStartRightPos = trackPosition(editor, 3, "right");
+    const getPosAfterPos = trackPosition(editor, 4);
+    const getPosAfterRightPos = trackPosition(editor, 4, "right");
+    // Insert text at the beginning
+    editor._tiptapEditor.commands.insertContentAt(3, "Test ");
+
+    // Position should be updated
+    expect(getCursorPos()).toBe(11); // 6 + 5 ("Test " length)
+    expect(getStartPos()).toBe(3); // 3
+    expect(getStartRightPos()).toBe(8); // 3 + 5 ("Test " length)
+    expect(getPosAfterPos()).toBe(9); // 4 + 5 ("Test " length)
+    expect(getPosAfterRightPos()).toBe(9); // 4 + 5 ("Test " length)
+  });
+
+  it("should handle multiple transactions", () => {
+    editor.replaceBlocks(editor.document, [
+      {
+        type: "paragraph",
+        content: "Hello World",
+      },
+    ]);
+
+    // Store position at "Hello| World"
+    const getCursorPos = trackPosition(editor, 6);
+    const getStartPos = trackPosition(editor, 3);
+    const getStartRightPos = trackPosition(editor, 3, "right");
+    const getPosAfterPos = trackPosition(editor, 4);
+    const getPosAfterRightPos = trackPosition(editor, 4, "right");
+
+    // Insert text at the beginning
+    editor._tiptapEditor.commands.insertContentAt(3, "T");
+    editor._tiptapEditor.commands.insertContentAt(4, "e");
+    editor._tiptapEditor.commands.insertContentAt(5, "s");
+    editor._tiptapEditor.commands.insertContentAt(6, "t");
+    editor._tiptapEditor.commands.insertContentAt(7, " ");
+
+    // Position should be updated
+    expect(getCursorPos()).toBe(11); // 6 + 5 ("Test " length)
+    expect(getStartPos()).toBe(3); // 3
+    expect(getStartRightPos()).toBe(8); // 3 + 5 ("Test " length)
+    expect(getPosAfterPos()).toBe(9); // 4 + 5 ("Test " length)
+    expect(getPosAfterRightPos()).toBe(9); // 4 + 5 ("Test " length)
+  });
+});
+
+describe("PositionStorage with remote editor", () => {
+  // Function to sync two documents
+  function syncDocs(sourceDoc: Y.Doc, targetDoc: Y.Doc) {
+    // Create update message from source
+    const update = Y.encodeStateAsUpdate(sourceDoc);
+
+    // Apply update to target
+    Y.applyUpdate(targetDoc, update);
+  }
+
+  // Set up two-way sync
+  function setupTwoWaySync(doc1: Y.Doc, doc2: Y.Doc) {
+    // Sync initial states
+    syncDocs(doc1, doc2);
+    syncDocs(doc2, doc1);
+
+    // Set up observers for future changes
+    doc1.on("update", (update: Uint8Array) => {
+      Y.applyUpdate(doc2, update);
+    });
+
+    doc2.on("update", (update: Uint8Array) => {
+      Y.applyUpdate(doc1, update);
+    });
+  }
+
+  describe("remote editor", () => {
+    let localEditor: BlockNoteEditor;
+    let remoteEditor: BlockNoteEditor;
+    let ydoc: Y.Doc;
+    let remoteYdoc: Y.Doc;
+
+    beforeEach(() => {
+      ydoc = new Y.Doc();
+      remoteYdoc = new Y.Doc();
+      // Create a mock editor
+      localEditor = BlockNoteEditor.create({
+        collaboration: {
+          fragment: ydoc.getXmlFragment("doc"),
+          user: { color: "#ff0000", name: "Local User" },
+          provider: undefined,
+        },
+      });
+      const div = document.createElement("div");
+      localEditor.mount(div);
+
+      remoteEditor = BlockNoteEditor.create({
+        collaboration: {
+          fragment: remoteYdoc.getXmlFragment("doc"),
+          user: { color: "#ff0000", name: "Remote User" },
+          provider: undefined,
+        },
+      });
+
+      const remoteDiv = document.createElement("div");
+      remoteEditor.mount(remoteDiv);
+      setupTwoWaySync(ydoc, remoteYdoc);
+    });
+
+    afterEach(() => {
+      ydoc.destroy();
+      remoteYdoc.destroy();
+      localEditor.mount(undefined);
+      localEditor._tiptapEditor.destroy();
+      remoteEditor.mount(undefined);
+      remoteEditor._tiptapEditor.destroy();
+    });
+
+    it("should update the local position when collaborating", () => {
+      localEditor.replaceBlocks(localEditor.document, [
+        {
+          type: "paragraph",
+          content: "Hello World",
+        },
+      ]);
+
+      // Store position at "Hello| World"
+      const getCursorPos = trackPosition(localEditor, 6);
+      // Store position at "|Hello World"
+      const getStartPos = trackPosition(localEditor, 3);
+      // Store position at "|Hello World" (but on the right side)
+      const getStartRightPos = trackPosition(localEditor, 3, "right");
+      // Store position at "H|ello World"
+      const getPosAfterPos = trackPosition(localEditor, 4);
+      // Store position at "H|ello World" (but on the right side)
+      const getPosAfterRightPos = trackPosition(localEditor, 4, "right");
+
+      // Insert text at the beginning
+      localEditor._tiptapEditor.commands.insertContentAt(3, "Test ");
+
+      // Position should be updated
+      expect(getCursorPos()).toBe(11); // 6 + 5 ("Test " length)
+      expect(getStartPos()).toBe(3); // 3
+      expect(getStartRightPos()).toBe(8); // 3 + 5 ("Test " length)
+      expect(getPosAfterPos()).toBe(9); // 4 + 5 ("Test " length)
+      expect(getPosAfterRightPos()).toBe(9); // 4 + 5 ("Test " length)
+    });
+
+    it("should handle multiple transactions when collaborating", () => {
+      localEditor.replaceBlocks(localEditor.document, [
+        {
+          type: "paragraph",
+          content: "Hello World",
+        },
+      ]);
+
+      // Store position at "Hello| World"
+      const getCursorPos = trackPosition(localEditor, 6);
+      // Store position at "|Hello World"
+      const getStartPos = trackPosition(localEditor, 3);
+      // Store position at "|Hello World" (but on the right side)
+      const getStartRightPos = trackPosition(localEditor, 3, "right");
+      // Store position at "H|ello World"
+      const getPosAfterPos = trackPosition(localEditor, 4);
+      // Store position at "H|ello World" (but on the right side)
+      const getPosAfterRightPos = trackPosition(localEditor, 4, "right");
+
+      // Insert text at the beginning
+      localEditor._tiptapEditor.commands.insertContentAt(3, "T");
+      localEditor._tiptapEditor.commands.insertContentAt(4, "e");
+      localEditor._tiptapEditor.commands.insertContentAt(5, "s");
+      localEditor._tiptapEditor.commands.insertContentAt(6, "t");
+      localEditor._tiptapEditor.commands.insertContentAt(7, " ");
+
+      // Position should be updated
+      expect(getCursorPos()).toBe(11); // 6 + 5 ("Test " length)
+      expect(getStartPos()).toBe(3); // 3
+      expect(getStartRightPos()).toBe(8); // 3 + 5 ("Test " length)
+      expect(getPosAfterPos()).toBe(9); // 4 + 5 ("Test " length)
+      expect(getPosAfterRightPos()).toBe(9); // 4 + 5 ("Test " length)
+    });
+
+    it("should update the local position from a remote transaction", () => {
+      remoteEditor.replaceBlocks(remoteEditor.document, [
+        {
+          type: "paragraph",
+          content: "Hello World",
+        },
+      ]);
+
+      // Store position at "Hello| World"
+      const getCursorPos = trackPosition(localEditor, 6);
+      // Store position at "|Hello World"
+      const getStartPos = trackPosition(localEditor, 3);
+      // Store position at "|Hello World" (but on the right side)
+      const getStartRightPos = trackPosition(localEditor, 3, "right");
+      // Store position at "H|ello World"
+      const getPosAfterPos = trackPosition(localEditor, 4);
+      // Store position at "H|ello World" (but on the right side)
+      const getPosAfterRightPos = trackPosition(localEditor, 4, "right");
+
+      // Insert text at the beginning
+      localEditor._tiptapEditor.commands.insertContentAt(3, "Test ");
+
+      // Position should be updated
+      expect(getCursorPos()).toBe(11); // 6 + 5 ("Test " length)
+      expect(getStartPos()).toBe(3); // 3
+      expect(getStartRightPos()).toBe(8); // 3 + 5 ("Test " length)
+      expect(getPosAfterPos()).toBe(9); // 4 + 5 ("Test " length)
+      expect(getPosAfterRightPos()).toBe(9); // 4 + 5 ("Test " length)
+    });
+
+    it("should update the remote position from a remote transaction", () => {
+      remoteEditor.replaceBlocks(remoteEditor.document, [
+        {
+          type: "paragraph",
+          content: "Hello World",
+        },
+      ]);
+
+      // Store position at "Hello| World"
+      const getCursorPos = trackPosition(remoteEditor, 6);
+      // Store position at "|Hello World"
+      const getStartPos = trackPosition(remoteEditor, 3);
+      // Store position at "|Hello World" (but on the right side)
+      const getStartRightPos = trackPosition(remoteEditor, 3, "right");
+      // Store position at "H|ello World"
+      const getPosAfterPos = trackPosition(remoteEditor, 4);
+      // Store position at "H|ello World" (but on the right side)
+      const getPosAfterRightPos = trackPosition(remoteEditor, 4, "right");
+
+      // Insert text at the beginning
+      localEditor._tiptapEditor.commands.insertContentAt(3, "Test ");
+
+      // Position should be updated
+      expect(getCursorPos()).toBe(11); // 6 + 5 ("Test " length)
+      expect(getStartPos()).toBe(3); // 3
+      expect(getStartRightPos()).toBe(8); // 3 + 5 ("Test " length)
+      expect(getPosAfterPos()).toBe(9); // 4 + 5 ("Test " length)
+      expect(getPosAfterRightPos()).toBe(9); // 4 + 5 ("Test " length)
+    });
+  });
+});
diff --git a/packages/core/src/api/positionMapping.ts b/packages/core/src/api/positionMapping.ts
new file mode 100644
index 000000000..1f89e7b32
--- /dev/null
+++ b/packages/core/src/api/positionMapping.ts
@@ -0,0 +1,112 @@
+import { Mapping } from "prosemirror-transform";
+import {
+  absolutePositionToRelativePosition,
+  relativePositionToAbsolutePosition,
+  ySyncPluginKey,
+} from "y-prosemirror";
+import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
+import * as Y from "yjs";
+import type { ProsemirrorBinding } from "y-prosemirror";
+
+/**
+ * This is used to track a mapping for each editor. The mapping stores the mappings for each transaction since the first transaction that was tracked.
+ */
+const editorToMapping = new Map<BlockNoteEditor<any, any, any>, Mapping>();
+
+/**
+ * This initializes a single mapping for an editor instance.
+ */
+function getMapping(editor: BlockNoteEditor<any, any, any>) {
+  if (editorToMapping.has(editor)) {
+    // Mapping already initialized, so we don't need to do anything
+    return editorToMapping.get(editor)!;
+  }
+  const mapping = new Mapping();
+  editor._tiptapEditor.on("transaction", ({ transaction }) => {
+    mapping.appendMapping(transaction.mapping);
+  });
+  editor._tiptapEditor.on("destroy", () => {
+    // Cleanup the mapping when the editor is destroyed
+    editorToMapping.delete(editor);
+  });
+
+  // There only is one mapping per editor, so we can just set it
+  editorToMapping.set(editor, mapping);
+
+  return mapping;
+}
+
+/**
+ * This is used to keep track of positions of elements in the editor.
+ * It is needed because y-prosemirror's sync plugin can disrupt normal prosemirror position mapping.
+ *
+ * It is specifically made to be able to be used whether the editor is being used in a collaboratively, or single user, providing the same API.
+ *
+ * @param editor The editor to track the position of.
+ * @param position The position to track.
+ * @param side The side of the position to track. "left" is the default. "right" would move with the change if the change is in the right direction.
+ * @returns A function that returns the position of the element.
+ */
+export function trackPosition(
+  /**
+   * The editor to track the position of.
+   */
+  editor: BlockNoteEditor<any, any, any>,
+  /**
+   * The position to track.
+   */
+  position: number,
+  /**
+   * This is the side of the position to track. "left" is the default. "right" would move with the change if the change is in the right direction.
+   */
+  side: "left" | "right" = "left"
+): () => number {
+  const ySyncPluginState = ySyncPluginKey.getState(editor.prosemirrorState) as {
+    doc: Y.Doc;
+    binding: ProsemirrorBinding;
+  };
+
+  if (!ySyncPluginState) {
+    // No y-prosemirror sync plugin, so we need to track the mapping manually
+    // This will initialize the mapping for this editor, if needed
+    const mapping = getMapping(editor);
+
+    // This is the start point of tracking the mapping
+    const trackedMapLength = mapping.maps.length;
+
+    return () => {
+      const pos = mapping
+        // Only read the history of the mapping that we care about
+        .slice(trackedMapLength)
+        .map(position, side === "left" ? -1 : 1);
+
+      return pos;
+    };
+  }
+
+  const relativePosition = absolutePositionToRelativePosition(
+    // Track the position after the position if we are on the right side
+    position + (side === "right" ? 1 : 0),
+    ySyncPluginState.binding.type,
+    ySyncPluginState.binding.mapping
+  );
+
+  return () => {
+    const curYSyncPluginState = ySyncPluginKey.getState(
+      editor.prosemirrorState
+    ) as typeof ySyncPluginState;
+    const pos = relativePositionToAbsolutePosition(
+      curYSyncPluginState.doc,
+      curYSyncPluginState.binding.type,
+      relativePosition,
+      curYSyncPluginState.binding.mapping
+    );
+
+    // This can happen if the element is garbage collected
+    if (pos === null) {
+      throw new Error("Position not found, cannot track positions");
+    }
+
+    return pos + (side === "right" ? -1 : 0);
+  };
+}
diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts
index aa8ffea6e..1a9a0abe7 100644
--- a/packages/core/src/editor/BlockNoteEditor.ts
+++ b/packages/core/src/editor/BlockNoteEditor.ts
@@ -723,6 +723,7 @@ export class BlockNoteEditor<
       // but we still need the schema
       this.pmSchema = getSchema(tiptapOptions.extensions!);
     }
+
     this.emit("create");
   }
 
diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts
index c21a8ddb5..26495d186 100644
--- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts
+++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts
@@ -10,6 +10,7 @@ import {
   StyleSchema,
 } from "../../schema/index.js";
 import { EventEmitter } from "../../util/EventEmitter.js";
+import { trackPosition } from "../../api/positionMapping.js";
 
 const findBlock = findParentNode((node) => node.type.name === "blockContainer");
 
@@ -129,7 +130,7 @@ class SuggestionMenuView<
       .focus()
       .deleteRange({
         from:
-          this.pluginState.queryStartPos! -
+          this.pluginState.queryStartPos() -
           (this.pluginState.deleteTriggerCharacter
             ? this.pluginState.triggerCharacter!.length
             : 0),
@@ -143,7 +144,7 @@ type SuggestionPluginState =
   | {
       triggerCharacter: string;
       deleteTriggerCharacter: boolean;
-      queryStartPos: number;
+      queryStartPos: () => number;
       query: string;
       decorationId: string;
       ignoreQueryLength?: boolean;
@@ -220,13 +221,22 @@ export class SuggestionMenuProseMirrorPlugin<
             suggestionPluginTransactionMeta !== null &&
             prev === undefined
           ) {
+            const trackedPosition = trackPosition(
+              editor,
+              newState.selection.from -
+                // Need to account for the trigger char that was inserted, so we offset the position by the length of the trigger character.
+                suggestionPluginTransactionMeta.triggerCharacter.length
+            );
             return {
               triggerCharacter:
                 suggestionPluginTransactionMeta.triggerCharacter,
               deleteTriggerCharacter:
                 suggestionPluginTransactionMeta.deleteTriggerCharacter !==
                 false,
-              queryStartPos: newState.selection.from,
+              // When reading the queryStartPos, we offset the result by the length of the trigger character, to make it easy on the caller
+              queryStartPos: () =>
+                trackedPosition() +
+                suggestionPluginTransactionMeta.triggerCharacter.length,
               query: "",
               decorationId: `id_${Math.floor(Math.random() * 0xffffffff)}`,
               ignoreQueryLength:
@@ -252,7 +262,7 @@ export class SuggestionMenuProseMirrorPlugin<
             transaction.getMeta("pointer") ||
             // Moving the caret before the character which triggered the menu should hide it.
             (prev.triggerCharacter !== undefined &&
-              newState.selection.from < prev.queryStartPos!)
+              newState.selection.from < prev.queryStartPos())
           ) {
             return undefined;
           }
@@ -261,7 +271,7 @@ export class SuggestionMenuProseMirrorPlugin<
 
           // Updates the current query.
           next.query = newState.doc.textBetween(
-            prev.queryStartPos!,
+            prev.queryStartPos(),
             newState.selection.from
           );
 
@@ -324,9 +334,9 @@ export class SuggestionMenuProseMirrorPlugin<
           // Creates an inline decoration around the trigger character.
           return DecorationSet.create(state.doc, [
             Decoration.inline(
-              suggestionPluginState.queryStartPos! -
+              suggestionPluginState.queryStartPos() -
                 suggestionPluginState.triggerCharacter!.length,
-              suggestionPluginState.queryStartPos!,
+              suggestionPluginState.queryStartPos(),
               {
                 nodeName: "span",
                 class: "bn-suggestion-decorator",