diff --git a/apps/docs/core/superdoc/methods.mdx b/apps/docs/core/superdoc/methods.mdx
index 5f479d0643..a9e5030300 100644
--- a/apps/docs/core/superdoc/methods.mdx
+++ b/apps/docs/core/superdoc/methods.mdx
@@ -572,6 +572,61 @@ const superdoc = new SuperDoc({
+### `scrollToComment`
+
+Scroll to a comment in the document and set it as active.
+
+
+ The comment ID to scroll to
+
+
+
+ Scroll behavior options
+
+
+ Scroll behavior — `"smooth"`, `"instant"`, or `"auto"`
+
+
+ Vertical alignment — `"start"`, `"center"`, `"end"`, or `"nearest"`
+
+
+
+
+**Returns:** `boolean` — `true` if the comment was found, `false` otherwise.
+
+
+
+```javascript Usage
+// Get a comment ID from the document API
+const { items } = superdoc.editor.doc.comments.list();
+const commentId = items[0].id;
+
+// Scroll to it
+superdoc.scrollToComment(commentId);
+```
+
+```javascript Full Example
+import { SuperDoc } from 'superdoc';
+import 'superdoc/style.css';
+
+const superdoc = new SuperDoc({
+ selector: '#editor',
+ document: yourFile,
+ modules: { comments: {} },
+ onReady: (superdoc) => {
+ const { items } = superdoc.editor.doc.comments.list();
+ if (items.length > 0) {
+ superdoc.scrollToComment(items[0].id, {
+ behavior: 'smooth',
+ block: 'center',
+ });
+ }
+ },
+});
+```
+
+
+
## User management
### `addSharedUser`
diff --git a/apps/docs/modules/comments.mdx b/apps/docs/modules/comments.mdx
index 0ec62ecfb5..cf3d65fadf 100644
--- a/apps/docs/modules/comments.mdx
+++ b/apps/docs/modules/comments.mdx
@@ -546,6 +546,39 @@ const superdoc = new SuperDoc({
+### `scrollToComment`
+
+Scroll the document to a comment and set it as active. Unlike `setCursorById`, this is a top-level SuperDoc method that works without accessing the editor directly.
+
+
+
+```javascript Usage
+superdoc.scrollToComment("comment-123");
+```
+
+```javascript Full Example
+import { SuperDoc } from 'superdoc';
+import 'superdoc/style.css';
+
+const superdoc = new SuperDoc({
+ selector: '#editor',
+ document: yourFile,
+ modules: { comments: {} },
+ onReady: (superdoc) => {
+ const { items } = superdoc.editor.doc.comments.list();
+ if (items.length > 0) {
+ superdoc.scrollToComment(items[0].id);
+ }
+ },
+});
+```
+
+
+
+
+ See [SuperDoc Methods](/core/superdoc/methods#scrolltocomment) for full parameter documentation.
+
+
## Events
### `onCommentsUpdate`
diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue
index c818272106..453dc4d773 100644
--- a/packages/superdoc/src/SuperDoc.vue
+++ b/packages/superdoc/src/SuperDoc.vue
@@ -1028,14 +1028,7 @@ watch(showCommentsSidebar, (value) => {
* @param {String} commentId The commentId to scroll to
*/
const scrollToComment = (commentId) => {
- const commentsConfig = proxy.$superdoc.config?.modules?.comments;
- if (!commentsConfig || commentsConfig === false) return;
-
- const element = document.querySelector(`[data-thread-id=${commentId}]`);
- if (element) {
- element.scrollIntoView({ behavior: 'smooth', block: 'start' });
- commentsStore.setActiveComment(proxy.$superdoc, commentId);
- }
+ proxy.$superdoc.scrollToComment(commentId);
};
onMounted(() => {
diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js
index e639728175..cb5d29c596 100644
--- a/packages/superdoc/src/core/SuperDoc.js
+++ b/packages/superdoc/src/core/SuperDoc.js
@@ -803,6 +803,29 @@ export class SuperDoc extends EventEmitter {
}
}
+ /**
+ * Scroll the document to a given comment by id.
+ *
+ * @param {string} commentId The comment id
+ * @param {{ behavior?: ScrollBehavior, block?: ScrollLogicalPosition }} [options]
+ * @returns {boolean} Whether a matching element was found
+ */
+ scrollToComment(commentId, options = {}) {
+ const commentsConfig = this.config?.modules?.comments;
+ if (!commentsConfig || commentsConfig === false) return false;
+ if (!commentId || typeof commentId !== 'string') return false;
+
+ const root = this.element || document;
+ const escaped = globalThis.CSS?.escape ? globalThis.CSS.escape(commentId) : commentId.replace(/"/g, '\\"');
+ const element = root.querySelector(`[data-comment-ids*="${escaped}"]`);
+ if (!element) return false;
+
+ const { behavior = 'smooth', block = 'start' } = options ?? {};
+ element.scrollIntoView({ behavior, block });
+ this.commentsStore?.setActiveComment?.(this, commentId);
+ return true;
+ }
+
/**
* Toggle the custom context menu globally.
* Updates both flow editors and PresentationEditor instances so downstream listeners can short-circuit early.
diff --git a/packages/superdoc/src/core/SuperDoc.test.js b/packages/superdoc/src/core/SuperDoc.test.js
index 804f473317..784eef1021 100644
--- a/packages/superdoc/src/core/SuperDoc.test.js
+++ b/packages/superdoc/src/core/SuperDoc.test.js
@@ -280,6 +280,44 @@ describe('SuperDoc core', () => {
expect(instance.user).toEqual(expect.objectContaining({ name: 'Default SuperDoc user', email: null }));
});
+ it('scrolls to a comment and sets it active', async () => {
+ const { commentsStore } = createAppHarness();
+ const instance = new SuperDoc({
+ selector: '#host',
+ document: 'https://example.com/doc.docx',
+ documents: [],
+ modules: { comments: {}, toolbar: {} },
+ colors: ['red'],
+ user: { name: 'Jane', email: 'jane@example.com' },
+ onException: vi.fn(),
+ });
+ await flushMicrotasks();
+
+ const target = document.createElement('div');
+ target.setAttribute('data-comment-ids', 'comment-1');
+ target.scrollIntoView = vi.fn();
+ document.querySelector('#host').appendChild(target);
+
+ const result = instance.scrollToComment('comment-1');
+ expect(result).toBe(true);
+ expect(target.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' });
+ expect(commentsStore.setActiveComment).toHaveBeenCalledWith(instance, 'comment-1');
+ });
+
+ it('returns false when comment element is not found', async () => {
+ createAppHarness();
+ const instance = new SuperDoc({
+ selector: '#host',
+ document: 'https://example.com/doc.docx',
+ documents: [],
+ modules: { comments: {}, toolbar: {} },
+ onException: vi.fn(),
+ });
+ await flushMicrotasks();
+
+ expect(instance.scrollToComment('nonexistent-id')).toBe(false);
+ });
+
it('warns when both document object and documents list provided', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createAppHarness();
diff --git a/tests/behavior/tests/comments/scroll-to-comment.spec.ts b/tests/behavior/tests/comments/scroll-to-comment.spec.ts
new file mode 100644
index 0000000000..933bb2c9ce
--- /dev/null
+++ b/tests/behavior/tests/comments/scroll-to-comment.spec.ts
@@ -0,0 +1,51 @@
+import { test, expect } from '../../fixtures/superdoc.js';
+import { addCommentByText, assertDocumentApiReady } from '../../helpers/document-api.js';
+
+test.use({ config: { toolbar: 'full', comments: 'on' } });
+
+test('scrollToComment scrolls to the comment and activates it', async ({ superdoc }) => {
+ await assertDocumentApiReady(superdoc.page);
+
+ // Create enough content so the comment is off-screen
+ for (let i = 0; i < 30; i++) {
+ await superdoc.type(`Line ${i}`);
+ await superdoc.newLine();
+ }
+ await superdoc.type('target text');
+ await superdoc.waitForStable();
+
+ const commentId = await addCommentByText(superdoc.page, {
+ pattern: 'target text',
+ text: 'scroll test comment',
+ });
+ await superdoc.waitForStable();
+ await superdoc.assertCommentHighlightExists({ text: 'target text', timeoutMs: 20_000 });
+
+ // Scroll to the top so the comment is out of view
+ await superdoc.page.evaluate(() => {
+ document.querySelector('.superdoc')?.scrollTo({ top: 0 });
+ });
+ await superdoc.waitForStable();
+
+ // Call scrollToComment via the public API.
+ // WebKit can lag on DOM attribute propagation, so poll until it succeeds.
+ await expect
+ .poll(async () => superdoc.page.evaluate((id) => (window as any).superdoc.scrollToComment(id), commentId), {
+ timeout: 10_000,
+ })
+ .toBe(true);
+
+ // Verify the comment highlight is now visible in the viewport
+ const highlight = superdoc.page.locator('.superdoc-comment-highlight').filter({ hasText: 'target text' });
+ await expect(highlight.first()).toBeVisible({ timeout: 5_000 });
+});
+
+test('scrollToComment returns false for a nonexistent comment', async ({ superdoc }) => {
+ await assertDocumentApiReady(superdoc.page);
+
+ const result = await superdoc.page.evaluate(() => {
+ return (window as any).superdoc.scrollToComment('nonexistent-id');
+ });
+
+ expect(result).toBe(false);
+});