Custom nodes reference

Custom node API: every option, every error code, the on-disk format.

For a working integration, see Custom nodes.

Define and register nodes

A node keeps its identity in w:tag. Its payload goes in a customXml part that the control binds through w:dataBinding.

defineCustomNode options

OptionTypeDescription
namestringNode type name, the second segment of the tag.
tagPrefixstringThe prefix this definition claims. docx claims docx:*.
schemaAny Standard SchemaPayload shape. Supply your own library. Asynchronous schemas are refused.
text(data) => stringDocument text, from the schema's output type.
label, chrome.colorstringChip display name (default name) and tint.
reviewCard({ attrs, text, data }) => { title, detail } | nullOne sidebar card per node. Requires the review module. data, when present, is the schema's output type.
onClick, onHover, onEdit(node: ActivatedCustomNode) => voidChip pointer events, and the context menu's "Edit {label}" row.
preserveOnExportboolean | 'text'Export policy. See Export nodes.
tagAttrs(data) => Record<string, string>Tag attributes that identify the node without its payload.
payloadNamespacestringcustomXml namespace. Defaults to one from tagPrefix. Quotes, angle brackets, and ampersands are rejected.
fromDocx({ attrs, text, data }) => attrs | nullRecognizes a schemaless node from its tag, or returns null to skip it. Writes then pass attrs and text. data, when present, is the schema's output type.

The returned CustomNode also carries dataOf.

customNodesModule options

OptionTypeDescription
nodesreadonly AnyCustomNodeDefinition[]The definitions this editor recognizes.
onDiagnostic({ code, name, nodeId, issues }) => voidReports 'payload-invalid' or 'payload-missing'. Each editor calls only its own listener, until disposal.
licenseKeystringOptional. Never validated and never sent over the network.

Write nodes

insertCustomNode(editor, def, input); // at the caret, or at input.at
updateCustomNode(editor, def, nodeId, update); // replaces the control
removeCustomNode(editor, nodeId); // deletes the node and its payload

Each write is one transaction and one undo step, including the payload.

Input fieldTypeDescription
dataThe schema's input typeThe payload. Validated before any write. updateCustomNode keeps an omitted payload; data: null removes it.
attrsRecord<string, string>Tag attributes. Overrides tagAttrs.
textstringDocument text. Overrides the definition's text.
at{ paragraphId, offset }Insert position. Defaults to the caret. insertCustomNode only.
lockfalse | 'sdtLocked' | 'sdtContentLocked' | 'contentLocked'insertCustomNode defaults to contentLocked. updateCustomNode keeps the existing lock.
aliasstringw:alias, the title shown on the control.

Results

A write returns { ok: true, changed: true, nodeId }. After an update, the old ID no longer resolves. removeCustomNode returns no nodeId.

A refusal carries a reason and a code:

codeMeaning
invalidArgsThe call is invalid: a payload past the cap, a tag over 64 characters, or an offset outside the paragraph.
unsupportedDocument state blocks the write: a lock, a protected form, or a bound control.
lockedThe document is open for viewing.
notFoundNo document is mounted, or updateCustomNode received an unknown ID.

A payload the schema rejected also carries issues. Each issue has a message, a path such as ['authors', 0], and a pointer such as authors.0.

Tracking and locks

  • In suggesting mode, insertCustomNode makes a tracked insertion. Without an author, it inserts untracked, as typing does.
  • updateCustomNode refuses a node that is still a pending proposal.
  • removeCustomNode is never tracked and is refused in viewing mode. Removing a proposed node removes the proposal.
  • Beside a contentLocked node, the first Backspace or Delete selects it and the second removes it. In suggesting mode, this is one tracked deletion.
  • A node with a payload refuses content edits inside it, whatever its lock.

Read nodes

  • customNodesOf(editor, options?) returns nodes from every story as a fresh snapshot: the body in document order, then headers, footers, footnotes, and endnotes. { nodes } narrows the definitions.
  • Citation.dataOf(node) returns the schema's output type. Otherwise it returns undefined: another definition, no payload, or a rejected payload. It accepts { data } without name; a supplied name must match the definition.
  • Without a registered definition, a node shows its content as an ordinary control, and the editor preserves it.
  • With the review module, editor.getReviewItems() lists node ranges as item.kind === 'custom' entries.
  • A payload without a schema is parsed JSON on a null-prototype object.
  • A node without reviewCard has no card or active highlight. setActiveReviewItem refuses its key with unsupported.

Export nodes

editor.save() always keeps nodes intact. saveForExport(editor, options?) applies each definition's preserveOnExport. Without an editor, prepareForExport(bytes, definitions) runs the same pipeline without DOM globals.

preserveOnExportExternal export result
true (default)Keeps the control, tag, binding, payload, and text.
'text'Keeps the text; removes the control, binding, and payload.
falseRemoves the control, its content, and its payload.

saveForExport returns counts in unwrapped and removed, or a reason and no bytes. options.nodes narrows the definitions. Set destination: 'internal' to return the saved bytes unchanged.

  • Export changes only nodes whose definitions you supply, in every story. List every definition that the document might hold.
  • When the last node of a namespace goes, its parts, relationships, and content-type override go too.
  • Export does not anonymize a document. It leaves docProps, comment and revision authors, rsids, and custom properties unchanged.

Author nodes on a server

customNodeXml(definition, attrs, text, options) builds the control XML without an editor or DOM:

import { customNodeXml } from '@docx-editor.dev/pro';

const built = customNodeXml(Citation, { sourceId: 'smith-2024' }, '(Smith 2024)', {
  data: { sourceId: 'smith-2024', author: 'Smith', year: 2024 },
});
if (built.ok) template.replace('{{citation}}', built.xml);

For a node with a payload, built.store returns two customXml parts, two relationships, and one content-type override. Add all five, or a DOCX application can report the file as damaged.

encodeCustomNodeTag and decodeCustomNodeTag read and write tags directly, up to MAX_TAG_LENGTH. Definitions that share a payloadNamespace share one store.

Limits

  • Sizes: Tags allow 64 characters, including the prefix and name. Payloads allow 262,144 UTF-16 code units (MAX_CUSTOM_NODE_DATA_LENGTH) on write and read. Labels allow 4,096 on write.
  • Prototype keys: Payloads lose __proto__, constructor, and prototype keys at every depth.
  • Untrusted attributes: attrs and text in fromDocx come from the file. Sanitize them before you build URLs or DOM.
  • Clipboard: Cut and copy carry the chip's text only. Paste inserts plain text.
  • Payload cleanup: On open, the editor collects payloads whose control is gone. This sweep cannot be undone.
  • Interoperability: Other DOCX applications keep the tag, binding, and payload, and show bound controls as read-only.

See also