Skip to Content
Header & Footer

Header & Footer

Headers and footers are editable regions at the top and bottom of each page in a paginated document. They are part of the section node model (section_header / section_footer, each holding one header_footer_content per variant) and are only meaningful with the pagination stack mounted.

Packages

PackagePurpose
@stesura/paginationheaderFooterBindingPlugin + headerFooterSyncPlugin, variants store, DOM/variant helpers
@stesura/pagination-reactHeaderFooterPanel, section header/footer node views, paginationUiExtension
@stesura/editor-reactuseHeaderFooterEditing / useSetHeaderFooterEditing editing state
@stesura/coreCommands (page numbers, distances, variant content, healing), headerFooterGuardPlugin, the headerFooterEditorRegistry of mounted header/footer editors

Setup

Nothing beyond the standard pagination setup: paginationUiExtension already registers SectionNodeview, SectionHeaderNodeView and SectionFooterNodeView, and mounts HeaderFooterPanel in the editor’s documentOverlaySlot:

import { useLocalEditor } from "@stesura/editor-react/hooks"; import { paginationPlugins } from "@stesura/pagination"; import { paginationUiExtension } from "@stesura/pagination-react"; const Editor = () => { const { editorState, dispatch, pluginFactory, schema } = useLocalEditor({ extraPlugins: () => [...paginationPlugins()], }); return ( <StesuraEditor state={editorState} dispatchTransaction={dispatch} schema={schema} pluginFactory={pluginFactory} uiExtensions={[paginationUiExtension]} /> ); };

HeaderFooterPanel hosts its own sub-editor for the part being edited; the toolbar targets it automatically while it is open. Double-clicking a rendered header or footer opens it. Only import HeaderFooterPanel from @stesura/pagination-react directly if you build a fully custom UI extension.

Opening the editor programmatically

Editing state is a piece of React state, read and written through hooks from @stesura/editor-react. The value is { sectionId, target, variant, focusOnOpen? } | null:

import { useHeaderFooterEditing, useSetHeaderFooterEditing } from "@stesura/editor-react"; const setEditing = useSetHeaderFooterEditing(); // Open the default footer of a section: setEditing({ sectionId, target: "footer", variant: "default" }); // Open without stealing DOM focus (e.g. from search navigation): setEditing({ sectionId, target: "header", variant: "first", focusOnOpen: false }); // Close the panel: setEditing(null); const editing = useHeaderFooterEditing(); // current state, or null

sectionId is the section node’s id attr; variant is "default" | "first" | "even".

The core commands focusHeaderFooter, openHeader and openFooter do not open the panel: they set a headerFooterSyncPluginKey meta (openHeaderId / openFooterId) that nothing reads. The selection focusHeaderFooter places in the header/footer is also moved back into the body by headerFooterGuardPlugin (see below). Use setEditing instead.

The mounted header/footer editors are listed in headerFooterEditorRegistry (from @stesura/core/stores), keyed per main editor by { sectionId, target, variant }:

import { headerFooterEditorRegistry } from "@stesura/core/stores"; // The open footer editor's EditorView, if mounted: const entry = headerFooterEditorRegistry.get(mainEditorId, { sectionId, target: "footer", variant: "default", }); entry?.view;

The generic sub-editor machinery behind it (derived schema, main→inner binding plugin, step forwarding) lives on @stesura/core/sub-editor.

Variants

Whether a section has a different first-page or even-page header/footer is derived from the document: a variant is on when its header_footer_content node exists. Toggle it with the core commands below, as the panel’s variant tabs do. headerFooterSyncPlugin mirrors the result into a @stesura/pagination store (nanostores) that rendering and the layout engine read. Don’t write that store with setHeaderFooterVariants: the next sync overwrites it.

import { getOrCreateSectionVariantsAtom, getVariantForPage } from "@stesura/pagination"; // The section's non-default variants, e.g. Set { "first" }: const variants = getOrCreateSectionVariantsAtom(view.state, sectionId).get(); // Which variant a section-local, 0-based page index renders (falls back to "default"): getVariantForPage(0, variants); // "first"

“First page” is per section, but odd/even follows document page parity, as in Word. For a section that doesn’t start the document, pass the number of pages before it as the third argument (pageOffset); useDocPageNumbering(sectionId) returns it.

Subscribe through the getOrCreate* accessors (getOrCreateSectionVariantsAtom, getOrCreateHeaderFooterContentAtom): they return the atom the sync plugin updates in place, even before the first sync. getHeaderFooterDomId(sectionId, target, variant) returns the DOM id of a rendered part.

Each variant’s content is a header_footer_content node in the document. These core commands add or remove one in the header and the footer together, keeping their variant sets equal:

import { insertHeaderFooterContent, deleteHeaderFooterContent } from "@stesura/core/commands"; // Adds an empty "first" content where it is missing; false if both already have one: insertHeaderFooterContent(sectionId, "first")(state, dispatch); // Removes "first" from header and footer; refuses "default": deleteHeaderFooterContent(sectionId, "first")(state, dispatch);

Page numbers

page_number is an inline field node (pageType: "currentPage" | "totalPages") that pagination replaces with the real number on each page. Both are document-wide, as in Word. totalPages is the whole document’s page count (Word’s NUMPAGES). currentPage continues across sections unless a section’s pageNumbering attr ({ start, format }, Word’s w:pgNumType) restarts it or sets its format (decimal, lowerRoman, upperRoman, lowerLetter, upperLetter). DOCX import sets that attr; there is no command for it yet. Commands from @stesura/core/commands:

import { insertPageNumber, insertPageOfPage, insertPageNumberPreset } from "@stesura/core/commands"; // At the cursor (typically inside an open header/footer editor): insertPageNumber({ pageType: "currentPage" })(state, dispatch); // "3 of 12": two fields around a separator word (default "of"; pass the bare // word, spaces are added): insertPageOfPage()(state, dispatch); // Word-style preset: a borderless 1×3 table in the header (top) or footer // (bottom), with the number in the left/center/right cell. Dispatch on the // MAIN editor state; it creates the variant's content if missing, all in one // transaction: insertPageNumberPreset({ sectionId, position: { vertical: "bottom", horizontal: "center" }, pageType: "currentPage", // default variant: "default", // default })(state, dispatch);

The preset replaces the content when it is a single empty paragraph, and appends to it otherwise.

Distances from the page edge

headerFromTop / footerFromBottom are section attrs (px):

import { setHeaderFooterDistance } from "@stesura/core/commands"; // Current section, or pass a sectionId as the second argument: setHeaderFooterDistance({ headerFromTop: 48, footerFromBottom: 40 })(state, dispatch);

Editing headers/footers from code

In the main editor, header/footer content is protected by headerFooterGuardPlugin (from @stesura/core/plugins, on by default):

  • a transaction whose steps all fall inside headers/footers is cancelled;
  • a transaction that also edits the body goes through, then the changed headers/footers are restored to their previous content;
  • a selection that lands in a header/footer is moved into the section body.

Collab and undo/redo transactions are exempt. To edit header/footer content (or place the selection there) on the main editor, set this meta:

tr.setMeta("enableHeaderFooterEdits", true);

The core header/footer commands that write content (insertPageNumberPreset, insertHeaderFooterContent, deleteHeaderFooterContent, fixHeadersFooters) set it for you, and edits made in the open header/footer editor are forwarded with it.

Healing malformed structures

fixHeadersFooters repairs header/footer structure in one transaction: it resolves null variants, removes duplicates, gives header and footer the same variant set (always including default), and gives each content node at least one paragraph. It returns false when nothing needed fixing. Run it before mounting editors on documents from untrusted sources:

import { fixHeadersFooters } from "@stesura/core/commands"; fixHeadersFooters(state, dispatch);

Next steps

Last updated on