Footnotes
Footnotes let your readers annotate text with numbered notes that render at the bottom of the page — right above the footer, exactly like in Microsoft Word. Each footnote is anchored to a superscript reference marker in the body text. The footnotes area takes up part of the page's available space, so the more footnotes a page has, the less room remains for body content.
Footnotes always stay with their reference: if editing pushes the referencing text onto another page, its footnote moves to that page's footnotes area automatically.
Imported from DOCX
When you import a .docx file with the DOCX import
extension, footnotes from the
document are auto-applied to Pages — references, content, and numbering. No extra wiring required.
Enabling footnotes
Footnotes are disabled by default. Turn them on through the footnotes options group:
import { Pages } from '@tiptap-pro/extension-pages'
Pages.configure({
pageFormat: 'A4',
footer: 'Page {page} of {total}',
footnotes: {
enabled: true,
},
})That's all you need — the footnote reference node is registered automatically, and the per-page footnote areas render as soon as the document contains footnotes.
How footnotes behave
The mental model matches Microsoft Word:
- One marker, one note. Each footnote is a superscript number in the body text, paired with a numbered entry in the footnotes area of the page that contains the marker.
- Footnotes live above the footer. Each page renders its own footnotes area with a short separator rule, between the body content and the page footer.
- Footnotes consume page space. The area grows with its content and the body content area shrinks accordingly — adding footnotes to a full page pushes body text onto the next page.
- Footnotes follow their references. The area shows exactly the footnotes whose markers sit on that page. Reflowing text across pages moves the footnotes along with it; you never manage footnote placement yourself.
- Numbering is automatic and continuous. Footnotes are numbered
1, 2, 3…in document order. Inserting a footnote between two existing ones renumbers everything after it; deleting one closes the gap. Numbers are computed, never stored — they're always correct.
Inserting a footnote
Call insertFootnote() to add a footnote at the current selection:
editor.commands.insertFootnote()This inserts the reference marker after the selection (selected text is kept, like in Word), creates an empty footnote, and opens the footnotes editor with the caret placed inside the new footnote so the user can type its content immediately.
<button onClick={() => editor.commands.insertFootnote()}>Insert footnote</button>Editing footnotes
Users edit footnotes directly by double-clicking a page's footnotes area. This opens a fully featured Tiptap editor scoped to that page: it shows the footnotes that belong to the page, in numbered order, each editable like regular rich text. Double-clicking a specific footnote places the caret in that footnote.
Close the editor with Escape, the close button, or by double-clicking outside it. While a footnotes editor is open, the main document editor is temporarily non-editable — same behavior as the header and footer editors.
Custom extensions
The footnotes editor defaults to ConvertKit. Pass your own stack through footnotes.extensions to keep the schema consistent with your main editor:
import { ConvertKit } from '@tiptap-pro/extension-convert-kit'
Pages.configure({
footnotes: {
enabled: true,
extensions: [ConvertKit.configure({ table: false })],
},
})Collaboration uses the callback form
footnotes.extensions also accepts a (ctx) => Extensions callback that receives the Y field
name and Y.Doc the footnotes editor should bind to. Use this form when you're wiring
collaboration — see Footnotes and collaboration below.
Active editor state
While a footnotes editor is open, the extension exposes it through storage — the same pattern as headers and footers, so a unified toolbar works across all three:
activeEditor– The Tiptap Editor instance of the open footnotes editor (ornull)activeEditorType–'footnotes'while editing footnotes (alongside the existing'header'/'footer'/nullvalues)activePageNumber– The page whose footnotes are being editedfootnotesEditorOn/footnotesEditorOff– Subscribe/unsubscribe to events on the footnotes editor
useEffect(() => {
if (!editor) return
const syncActiveEditorState = () => {
const { activeEditor, activeEditorType } = editor.storage.pages
// activeEditorType === 'footnotes' while the footnotes editor is open
}
editor.on('update', syncActiveEditorState)
return () => {
editor.off('update', syncActiveEditorState)
}
}, [editor])For a complete custom-toolbar example that follows the focused editor, see Page header and footer → Building a custom toolbar — adding footnotes support to it only requires handling the additional 'footnotes' value of activeEditorType.
Locking the footnotes
Set footnotes.editable to false to render footnotes without the double-click editing affordance, or toggle at runtime:
Pages.configure({
footnotes: { enabled: true, editable: false },
})
// Runtime
editor.commands.setFootnotesEditable(false) // lock
editor.commands.setFootnotesEditable(true) // unlockWhen locked, footnotes still render normally — only editing is disabled. openFootnoteEditor returns false, double-click is inert, and an already-open footnotes editor is closed.
Preventing close on double-click
Like headers and footers, you can keep the footnotes editor open when the user double-clicks outside it — useful when your own toolbar sits outside the editor:
Pages.configure({
footnotes: { enabled: true },
onDblClickFootnotesPreventClose: (event) => {
const toolbar = document.querySelector('.my-toolbar')
return toolbar?.contains(event.target)
},
})The generic onDblClickHeaderFooterPreventClose callback acts as the fallback when the footnotes-specific one is not provided.
Programmatic open and close
// Open the footnotes editor for page 2
editor.commands.openFootnoteEditor({ pageNumber: 2 })
// Open it with the caret placed in a specific footnote
editor.commands.openFootnoteEditor({ pageNumber: 2, focusNoteId: '3' })
// Close it
editor.commands.closeFootnoteEditor()openFootnoteEditor returns false when footnotes are disabled, locked, or the page has no footnotes.
Deleting footnotes and cleaning up
Deleting a reference marker from the body immediately removes its footnote from the page — and renumbers the rest. The footnote's content is retained behind the scenes so a plain undo restores the marker together with its text. This deliberately diverges from Word (which deletes the content immediately) in favor of undo safety.
When you want to permanently discard content whose references are gone, call:
editor.commands.cleanupOrphanFootnotes()This returns true when at least one orphaned footnote was removed.
Copy & paste
Copying text that contains a footnote marker and pasting it elsewhere duplicates the footnote, Word-style: the pasted marker gets its own footnote with a copy of the original content, and the numbering updates across the document. The two footnotes are independent from that point on.
References without content
A reference marker always produces a visible footnote — even when no content exists for it yet (for example after setContent() with a document that contains markers but before any footnote content was provided). Such footnotes render as empty numbered entries, and opening the footnotes editor makes them immediately editable.
Navigating from a marker
Clicking a footnote marker in the body scrolls the page so its footnote is visible — handy in long documents.
Configuration
All footnote settings live in the footnotes options group:
Pages.configure({
footnotes: {
enabled: true, // master switch (default: false)
extensions: [ConvertKit], // editor extensions (or a collab-aware callback)
initialContent: undefined, // seed content, keyed by note id
separator: true, // the short rule above the area (default: true)
maxHeightRatio: 0.5, // max fraction of page content height (default: 0.5)
editable: true, // double-click editing (default: true)
accentColor: '#6366f1', // footnotes editor accent (defaults to accentColor)
},
})Seeding initial content
initialContent accepts footnote content keyed by note id — each id matching the noteId attribute of a footnoteReference node in your document content. The values are Tiptap JSONContent documents:
Pages.configure({
footnotes: {
enabled: true,
initialContent: {
1: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: 'The first footnote.' }],
},
],
},
},
},
})This is the exact shape the DOCX import REST API returns in its footnotes field, so server-imported documents can be seeded directly.
Collaboration
initialContent only applies when collaboration is not active — in collaborative documents
the shared document owns the footnote content. To load footnotes into a collaborative document as
an explicit user action (e.g. after a DOCX import), use the setFootnotes command instead.
Separator rule
Word draws a short horizontal rule between the body and the footnotes. It's on by default; disable it with separator: false.
Maximum area height
maxHeightRatio caps how much of the page's content height the footnotes area may take (default 0.5, i.e. half the page). When footnotes exceed the cap, the area clips. See What not to expect for the difference from Word here.
Accent color
The footnotes editor uses the shared accentColor by default; override it with footnotes.accentColor or at runtime:
editor.commands.setFootnotesAccentColor('#10b981')The accent affects the marker color in the body, the caret, the editor toolbar border, and the "Footnotes – page N" label.
Accessing footnote content
Footnote content is exposed on editor.storage.pages, keyed by note id:
// Tiptap JSON per footnote — use this for persistence and DOCX export
const footnotesJSON = editor.storage.pages.footnotesJSON
// { '1': { type: 'doc', content: [...] }, 'fn-abc123': { ... } }
// Rendered HTML per footnote — matches what the page previews show
const footnotesHTML = editor.storage.pages.footnotesHTML
// Current numbering (note id → 1-based number)
const numbers = editor.storage.pages.footnoteNumbersSaving and restoring
As with headers and footers, persisting footnote content is your responsibility. Save footnotesJSON alongside your document, and restore it with the setFootnotes command after loading the content:
// Save
await saveDocument({
content: editor.getJSON(),
footnotes: editor.storage.pages.footnotesJSON,
})
// Restore
editor.commands.setContent(savedDocument.content)
editor.commands.setFootnotes(savedDocument.footnotes)setFootnotes replaces all footnote content from an id → document map. References in the body keep working because they're matched by id.
DOCX import and export
Footnotes round-trip with Word documents out of the box:
- Import — with the DOCX import editor extension, footnotes from the imported file are auto-applied: markers appear in the body, content lands in the page areas, numbering matches the source document. The import context also exposes the raw
footnotes/endnotesdata if you want to handle them yourself. - Export — with the DOCX export editor extension, footnote content is auto-extracted from Pages and written as real Word footnotes: markers become live footnote references, and Word renders and renumbers them natively.
No configuration is needed on either side — install the import/export extensions next to Pages and footnotes are included.
Footnotes and collaboration
Footnotes participate in collaboration alongside the main document: concurrent users see each other's footnote edits live, and inserting or deleting footnotes converges across clients, including the numbering.
To opt in, pass footnotes.extensions as a callback that attaches a Collaboration extension to the footnotes editor — the same pattern headers and footers use:
Pages.configure({
footnotes: {
enabled: true,
extensions: (ctx) => {
const base = [ConvertKit.configure({ undoRedo: false })]
if (!ctx.isCollaborative || !ctx.ydoc) {
return base
}
return [...base, Collaboration.configure({ document: ctx.ydoc, field: ctx.field })]
},
},
})The callback receives the pre-computed Y field name (ctx.field) and the parent editor's Y.Doc (ctx.ydoc). See Adding collaboration to Pages for the full collaboration setup, including the equivalent wiring for headers and footers.
What to expect
- Word-like placement: footnotes at the bottom of the page that contains their marker, above the footer, with a separator rule.
- Automatic, continuous numbering that updates on every insert, delete, paste, and reflow.
- The page body shrinks as footnotes grow — pagination accounts for footnote space.
- Rich-text footnote content with the extensions you configure.
- Identical look between the rendered footnotes and the editing view — typography, numbering, and spacing match, including empty paragraphs used as vertical spacing.
- DOCX round-trip with no extra configuration.
What not to expect
- No footnote continuation across pages. When a page's footnotes exceed
maxHeightRatio, the area clips instead of continuing the overflow on the next page like Word does. - Endnotes are not rendered. The DOCX import surfaces endnote data for your own handling, but Pages doesn't display or manage endnotes.
- Decimal numbering only. Footnotes number
1, 2, 3…continuously through the document — per-page restarts and other formats (roman, letters, symbols) are not available yet. - Tables inside footnotes don't export. They render in the editor, but Word's footnote format only accepts paragraphs, so tables are dropped on DOCX export.
Help us prioritise
If one of these gaps blocks your use case, let us know — your feedback drives the roadmap.
Share your use case with Tiptap
Complete options reference
All options live under the footnotes key of Pages.configure() unless noted otherwise.
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Master switch for the footnotes feature |
extensions | Extensions | (ctx) => Extensions | ConvertKit | Extensions for the footnotes editor. Use the callback form for collaboration. |
initialContent | Record<string, JSONContent> | undefined | Seed footnote content keyed by note id (non-collaborative documents only) |
separator | boolean | true | Render the Word-style separator rule above the area |
maxHeightRatio | number | 0.5 | Maximum fraction of the page content height the area may occupy |
editable | boolean | true | Whether double-clicking the area opens the footnotes editor |
accentColor | string | accentColor value | Accent color for markers and the footnotes editor |
onDblClickFootnotesPreventClose | (event: MouseEvent) => boolean (top-level option) | undefined | Prevent closing the footnotes editor on double-click outside |
Commands reference
| Command | Parameters | Description |
|---|---|---|
insertFootnote | none | Insert a footnote at the selection and open its editor |
setFootnotes | footnotes: Record<string, JSONContent> | Replace all footnote content from an id → document map |
openFootnoteEditor | { pageNumber: number, focusNoteId?: string } | Open the footnotes editor for a page, optionally focusing a specific footnote |
closeFootnoteEditor | none | Close the footnotes editor if open |
setFootnotesEditable | enabled: boolean | Lock or unlock footnotes editing |
cleanupOrphanFootnotes | none | Permanently remove footnote content whose references are gone |
setFootnotesAccentColor | color: string | Set the footnotes accent color |
Storage reference
Read these from editor.storage.pages:
| Property | Type | Description |
|---|---|---|
footnotesJSON | Record<string, JSONContent> | Footnote content per note id — use for persistence and DOCX export |
footnotesHTML | Record<string, string> | Rendered HTML per note id, matching the page previews |
footnoteNumbers | Record<string, number> | Current numbering (note id → 1-based number) |
footnotesEnabled | boolean | Whether footnotes are enabled |
editableFootnotes | boolean | Whether editing is unlocked (read-only — use setFootnotesEditable) |
activeEditorType | 'header' | 'footer' | 'footnotes' | null | 'footnotes' while the footnotes editor is open |
footnotesEditorOn | Editor['on'] | null | Pre-bound subscriber for events on the footnotes editor |
footnotesEditorOff | Editor['off'] | null | Pre-bound unsubscriber for the footnotes editor |