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
| Option | Type | Description |
|---|---|---|
name | string | Node type name, the second segment of the tag. |
tagPrefix | string | The prefix this definition claims. docx claims docx:*. |
schema | Any Standard Schema | Payload shape. Supply your own library. Asynchronous schemas are refused. |
text | (data) => string | Document text, from the schema's output type. |
label, chrome.color | string | Chip display name (default name) and tint. |
reviewCard | ({ attrs, text, data }) => { title, detail } | null | One sidebar card per node. Requires the review module. data, when present, is the schema's output type. |
onClick, onHover, onEdit | (node: ActivatedCustomNode) => void | Chip pointer events, and the context menu's "Edit {label}" row. |
preserveOnExport | boolean | 'text' | Export policy. See Export nodes. |
tagAttrs | (data) => Record<string, string> | Tag attributes that identify the node without its payload. |
payloadNamespace | string | customXml namespace. Defaults to one from tagPrefix. Quotes, angle brackets, and ampersands are rejected. |
fromDocx | ({ attrs, text, data }) => attrs | null | Recognizes 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
| Option | Type | Description |
|---|---|---|
nodes | readonly AnyCustomNodeDefinition[] | The definitions this editor recognizes. |
onDiagnostic | ({ code, name, nodeId, issues }) => void | Reports 'payload-invalid' or 'payload-missing'. Each editor calls only its own listener, until disposal. |
licenseKey | string | Optional. 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 payloadEach write is one transaction and one undo step, including the payload.
| Input field | Type | Description |
|---|---|---|
data | The schema's input type | The payload. Validated before any write. updateCustomNode keeps an omitted payload; data: null removes it. |
attrs | Record<string, string> | Tag attributes. Overrides tagAttrs. |
text | string | Document text. Overrides the definition's text. |
at | { paragraphId, offset } | Insert position. Defaults to the caret. insertCustomNode only. |
lock | false | 'sdtLocked' | 'sdtContentLocked' | 'contentLocked' | insertCustomNode defaults to contentLocked. updateCustomNode keeps the existing lock. |
alias | string | w: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:
code | Meaning |
|---|---|
invalidArgs | The call is invalid: a payload past the cap, a tag over 64 characters, or an offset outside the paragraph. |
unsupported | Document state blocks the write: a lock, a protected form, or a bound control. |
locked | The document is open for viewing. |
notFound | No 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,
insertCustomNodemakes a tracked insertion. Without an author, it inserts untracked, as typing does. updateCustomNoderefuses a node that is still a pending proposal.removeCustomNodeis never tracked and is refused in viewing mode. Removing a proposed node removes the proposal.- Beside a
contentLockednode, 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 returnsundefined: another definition, no payload, or a rejected payload. It accepts{ data }withoutname; a suppliednamemust 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 asitem.kind === 'custom'entries. - A payload without a
schemais parsed JSON on a null-prototype object. - A node without
reviewCardhas no card or active highlight.setActiveReviewItemrefuses its key withunsupported.
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.
preserveOnExport | External export result |
|---|---|
true (default) | Keeps the control, tag, binding, payload, and text. |
'text' | Keeps the text; removes the control, binding, and payload. |
false | Removes 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, andprototypekeys at every depth. - Untrusted attributes:
attrsandtextinfromDocxcome 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.