Adding collaboration to Pages
Pages supports real-time collaboration on the main document body, on every header/footer sub-editor, and on the document-level page geometry (page format, page gap, header/footer margin overrides) out of the box. Each header/footer subtype — default, first page, odd, even — gets its own Y field, so multiple users can edit them simultaneously and see remote changes propagate live into both the open editor and the page-level preview rendered on every page. Switching the page format or dragging a margin slider on one client updates the layout on every connected client.
How it works
When @tiptap/extension-collaboration is wired on the parent editor, the Pages extension hands you the Y.Doc through a headerFooterExtensions callback so you can attach a Collaboration extension to each header/footer sub-editor. The package does not bundle yjs or @tiptap/extension-collaboration — your application owns those dependencies and attaches them through the callback.
Every header/footer subtype (default, first page, odd, even — for both headers and footers) collaborates independently. The configuration toggles differentFirstPage / differentOddEven and the document-level page geometry (pageFormat, pageGap, headerTopMargin, footerBottomMargin) all sync across clients automatically — toggling a flag or changing the format on one client flips it on the others within a frame. Remote edits propagate into the page-level preview on every client even when no overlay is open.
If you also wire @tiptap/extension-collaboration-caret (CollaborationCursor in Tiptap v2) on the parent editor, remote carets and user labels appear in the main document body. The callback context exposes the provider and local user via ctx.cursor, but see the caveat below — the recommended setup keeps awareness on the main editor only, not on the sub-editors.
1. Requirements
- A Tiptap Pro plan that supports collaboration
- Access to Tiptap Cloud or your own collaboration backend
- The Pages stack:
@tiptap-pro/extension-convert-kit,@tiptap-pro/extension-pages-tablekit,@tiptap-pro/extension-pages - A collaborative provider:
@tiptap-pro/provider(or your own) plus@tiptap/extension-collaborationandyjs
2. Install the required packages
npm install @tiptap-pro/extension-convert-kit \
@tiptap-pro/extension-pages-tablekit \
@tiptap-pro/extension-pages \
@tiptap-pro/provider \
@tiptap/extension-collaboration \
yjs3. Set up your collaborative provider
import { TiptapCollabProvider } from '@tiptap-pro/provider'
import * as Y from 'yjs'
const doc = new Y.Doc()
const provider = new TiptapCollabProvider({
name: 'document.name', // Unique document identifier for syncing
appId: 'your-app-id', // From the Cloud dashboard, or use `baseURL` for on-premises
token: 'your-jwt', // JWT generated by your server
document: doc,
})4. Configure your editor with the Pages stack and collaboration
Use the callback form of headerFooterExtensions. Pages will call your callback once per subtype (default, first, odd, even × header, footer) with a context describing whether collaboration is active and what Y field the sub-editor should bind to. Return the extensions for that subtype, including a Collaboration extension when ctx.isCollaborative is true.
import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import { ConvertKit } from '@tiptap-pro/extension-convert-kit'
import { TableKit } from '@tiptap-pro/extension-pages-tablekit'
import { Pages } from '@tiptap-pro/extension-pages'
const editor = new Editor({
extensions: [
ConvertKit.configure({ table: false }),
TableKit,
Pages.configure({
pageFormat: 'A4',
// ⚠️ Do NOT pass `header` / `footer` defaults here — see "What to expect"
// below. With collaboration on, Y is the source of truth for header/footer
// content; defaults compete with that and can re-appear after a remote clear.
headerFooterExtensions: ctx => {
// Mirror the main editor's stack inside each sub-editor so the schema,
// marks, and tables behave identically there.
const base = [ConvertKit.configure({ table: false }), TableKit]
if (!ctx.isCollaborative || !ctx.ydoc) {
// No collaboration on the parent editor — return your base stack.
return base
}
return [
...base,
Collaboration.configure({
document: ctx.ydoc,
field: ctx.field,
}),
]
},
}),
Collaboration.configure({
document: doc,
}),
],
})One callback, eight sub-editors
Your callback is called once per (editorType, subType) pair — eight times in total. Each
invocation receives a distinct ctx.field so the matching sub-editor stays in sync with its own
slice of the document. Subtypes that aren't currently being edited still receive remote updates
in the background, so the page-level preview reflects changes immediately on every client.
Adding awareness with CollaborationCaret
Install @tiptap/extension-collaboration-caret (@tiptap/extension-collaboration-cursor in Tiptap v2) and attach it to the main editor. Remote carets and user labels then appear in the main document body:
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
const localUser = { name: 'Alice', color: '#3b82f6' }
const editor = new Editor({
extensions: [
ConvertKit.configure({ table: false }),
TableKit,
Pages.configure({
headerFooterExtensions: ctx => {
const base = [ConvertKit.configure({ table: false }), TableKit]
if (!ctx.isCollaborative || !ctx.ydoc) return base
// Content sync only — see the callout below for why
// CollaborationCaret is intentionally not forwarded here.
return [...base, Collaboration.configure({ document: ctx.ydoc, field: ctx.field })]
},
}),
Collaboration.configure({ document: doc }),
// Awareness on the main document body.
CollaborationCaret.configure({ provider, user: localUser }),
],
})Why we don't put CollaborationCaret on every sub-editor
y-prosemirror's awareness has a single cursor slot per client. With one
provider serving the main editor plus 8 header/footer sub-editors, every
yCursorPlugin instance writes to that same slot, and each editor then tries
to decode the others' positions against a different Y field. The render
fallback paints a wide "selection" bar across the entire header / footer
content rather than a thin caret.
Keep CollaborationCaret on the main editor only. Content still syncs
cleanly in every header/footer sub-editor; you just don't see remote carets
while editing inside an overlay. The ctx.cursor value is still passed to
the callback if you want to forward it to a single canonical sub-editor for
experimentation — but don't forward it to all eight.
What syncs
Content
- All eight header/footer sub-editors. Default / first page / odd / even × header / footer — every subtype stays in sync.
- Live previews on every client, even with the overlay closed. When client A types in the header, client B's rendered page-level header updates within a frame — B does not need to open an overlay to see the change.
- Cleared content stays cleared. Deleting all header content on one client leaves the rendered header blank on every other client.
- Heights adjust on remote growth. If a remote user adds enough lines to push a header beyond its current reserved space, every client's page layout adjusts so the body doesn't overlap.
Subtype toggles
differentFirstPage(and itsdifferentFirstPageFootercounterpart) toggled together on one client flip on the others, matching Microsoft Word's "both at once" behaviour.differentOddEven(and itsdifferentOddEvenFootercounterpart) — same.
Document-level page geometry
pageFormat— built-in formats (A4,Letter,Legal, …) and customPageFormatobjects.pageGap— the vertical space between pages.headerTopMargin— explicit override of the distance from page top to header content.footerBottomMargin— explicit override of the distance from footer to page bottom.
A few practical examples:
// Triggering any of these on one client propagates to every connected client.
editor.commands.setPageFormat('Letter')
editor.commands.setPageGap(80)
editor.commands.setHeaderTopMargin(40)
editor.commands.setFooterBottomMargin(40)The reset* margin commands propagate the reset, not the format default — resetHeaderTopMargin() clears the override on every client so they all fall back to the format's own default (50% of the format's top margin):
editor.commands.resetHeaderTopMargin()
editor.commands.resetFooterBottomMargin()Optional: awareness
If you wire @tiptap/extension-collaboration-cursor on the parent editor, remote carets and labels appear in the header/footer overlays too — see Adding CollaborationCursor below.
What does NOT sync
These stay per-client by design, because they describe how you are viewing the document rather than the document itself:
zoom— different screen sizes need different zoom. One user at 80%, another at 100% is fine and useful.accentColor/headerAccentColor/footerAccentColor— overlay chrome (caret, toolbar border, label background) only shows on your edits. Per-user lets people pick a high-contrast option for accessibility.pageGapBackground— visual chrome outside the page area, same reasoning as accent.editableHeader/editableFooter— these are permission gates. They should come from your app's auth/role model (admin can edit, viewer can't), not from per-document Y state. Otherwise any user could lock everyone out.
If you need any of these shared (for example, you want all clients to see the same accent), sync them through your own application state.
Caveats
- Don't pass
header/footerdefaults inPages.configure()when collaboration is on. With collaboration enabled the header/footer content is owned by Y, and a configured default can re-appear after another client has cleared the content. Leave the defaults out; if you need a starter value, set it once viaeditor.commands.setHeader(...)after the provider has synced. - The package does not bundle
yjsor@tiptap/extension-collaboration. They're peer dependencies — install them in your application and attachCollaborationthrough theheaderFooterExtensionscallback. - Toggling
differentOddEvendoes not copy content across the wire. When you enable it, no content is pre-populated from the default header into the odd slot for other clients. Each client sees what's actually been edited into each subtype.
The callback context
The callback receives a context with everything it needs to wire collaboration for one subtype:
import type { HeaderFooterExtensionsContext } from '@tiptap-pro/extension-pages'
interface HeaderFooterExtensionsContext {
/** Which sub-editor this invocation is for. */
editorType: 'header' | 'footer'
/** Which subtype (default / first / odd / even). */
subType: 'default' | 'first' | 'odd' | 'even'
/** True when the parent editor has Collaboration installed. */
isCollaborative: boolean
/** Y.Doc instance from the parent's Collaboration extension, or null. */
ydoc: import('yjs').Doc | null
/** Y field name to bind the sub-editor's Collaboration extension to. */
field: string
/** Awareness info from the parent's CollaborationCursor, or null. */
cursor: { provider: unknown; user: unknown } | null
}Pass ctx.field straight into Collaboration.configure({ document: ctx.ydoc, field: ctx.field }) — the extension routes each subtype to its own slice of the Y.Doc automatically.
Tips for a smooth experience
- Wait for the provider to sync before allowing edits. A common pattern is to gate
<EditorContent>behind aconnectedflag that flips on the provider'sonSyncedcallback. - Mirror the main editor's extensions in the callback's
base. Pass the sameConvertKit/TableKitconfiguration so the sub-editors accept the same marks and nodes. - Persistence is your job. Tiptap Cloud / your provider persists the
Y.Doc, which automatically covers the header/footer content, the synced toggles, and the document-level page geometry (format, gap, margin overrides). You only need to persist the main document body separately if you also want a non-collab fallback snapshot. - Test in two browser windows. Open the same document in two tabs and exercise the full sync surface: type in a header on one and watch the other's page-level preview update without opening the overlay; toggle a different-first-page flag and watch both buttons flip together; change the page format on one and watch the other's pages reflow; drag a margin slider on one and watch the other's page CSS recompute.
Next steps
- Explore Pages options for layout control
- See Page header and footer for editing UX details, locking, programmatic open/close, and DOCX-aware behavior
- See the Tiptap collaboration docs for advanced collaboration usage