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 (or null)
  • activeEditorType'footnotes' while editing footnotes (alongside the existing 'header' / 'footer' / null values)
  • activePageNumber – The page whose footnotes are being edited
  • footnotesEditorOn / 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) // unlock

When 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.

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.footnoteNumbers

Saving 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 / endnotes data 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.

OptionTypeDefaultDescription
enabledbooleanfalseMaster switch for the footnotes feature
extensionsExtensions | (ctx) => ExtensionsConvertKitExtensions for the footnotes editor. Use the callback form for collaboration.
initialContentRecord<string, JSONContent>undefinedSeed footnote content keyed by note id (non-collaborative documents only)
separatorbooleantrueRender the Word-style separator rule above the area
maxHeightRationumber0.5Maximum fraction of the page content height the area may occupy
editablebooleantrueWhether double-clicking the area opens the footnotes editor
accentColorstringaccentColor valueAccent color for markers and the footnotes editor
onDblClickFootnotesPreventClose(event: MouseEvent) => boolean (top-level option)undefinedPrevent closing the footnotes editor on double-click outside

Commands reference

CommandParametersDescription
insertFootnotenoneInsert a footnote at the selection and open its editor
setFootnotesfootnotes: 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
closeFootnoteEditornoneClose the footnotes editor if open
setFootnotesEditableenabled: booleanLock or unlock footnotes editing
cleanupOrphanFootnotesnonePermanently remove footnote content whose references are gone
setFootnotesAccentColorcolor: stringSet the footnotes accent color

Storage reference

Read these from editor.storage.pages:

PropertyTypeDescription
footnotesJSONRecord<string, JSONContent>Footnote content per note id — use for persistence and DOCX export
footnotesHTMLRecord<string, string>Rendered HTML per note id, matching the page previews
footnoteNumbersRecord<string, number>Current numbering (note id → 1-based number)
footnotesEnabledbooleanWhether footnotes are enabled
editableFootnotesbooleanWhether editing is unlocked (read-only — use setFootnotesEditable)
activeEditorType'header' | 'footer' | 'footnotes' | null'footnotes' while the footnotes editor is open
footnotesEditorOnEditor['on'] | nullPre-bound subscriber for events on the footnotes editor
footnotesEditorOffEditor['off'] | nullPre-bound unsubscriber for the footnotes editor