Skip to content

Unique ID

UniqueID automatically assigns unique IDs to block nodes. Every paragraph, heading, blockquote, list, and other configured node gets a UUID attribute. IDs are assigned on creation, and pasted content gets new IDs when duplicates are detected. Useful for collaborative editing, deep linking, change tracking, and content addressing.

Not included in StarterKit. Add it separately.

import { Document, Paragraph, Text, UniqueID } from '@domternal/core';
import { DomternalEditor } from '@domternal/vanilla';
import '@domternal/theme';
const dm = new DomternalEditor(document.getElementById('editor')!, {
extensions: [Document, Paragraph, Text, UniqueID],
});
// Each paragraph now has a unique id attribute
// <p id="a1b2c3d4-...">text</p>
OptionTypeDefaultDescription
typesstring[]11 node types (see below)Node types that receive unique IDs
attributeNamestring'id'HTML attribute name for the ID
generateID() => stringgenerateUUIDFunction to generate unique IDs
filterDuplicatesbooleantrueRegenerate IDs for duplicates when pasting

The default list covers all common block nodes:

paragraph, heading, blockquote, codeBlock, bulletList, orderedList, taskList, listItem, taskItem, image, horizontalRule

The built-in generator creates UUIDs in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx. You can replace it with any function that returns a unique string:

UniqueID.configure({
generateID: () => crypto.randomUUID(), // native browser API
})

Or shorter IDs:

UniqueID.configure({
generateID: () => Math.random().toString(36).slice(2, 10), // e.g. "k5f2m8n1"
})
UniqueID.configure({
attributeName: 'data-block-id', // renders as data-block-id="..." instead of id="..."
})
UniqueID.configure({
filterDuplicates: false, // allow pasted content to keep original IDs
})

Duplicate-id rename on setContent (v0.7.0)

Section titled “Duplicate-id rename on setContent (v0.7.0)”

When you call editor.setContent(...) or otherwise load content with colliding ids (corrupted saved snapshots, PM transactions that copy a node), the plugin now renames duplicate ids so each node ends up unique. First occurrence wins; subsequent duplicates receive freshly generated ids.

Paste from outside the editor goes through transformPasted first; the duplicate-rename pass is a safety net for setContent and bulk inserts. The result: the document always maintains a unique-id space required for native #hash anchors and getElementById.

The TableOfContents extension REQUIRES UniqueID to be loaded - it reads UniqueID’s attributeName (default 'id') from heading nodes to build navigation anchors. TableOfContents does NOT create its own id attribute; it relies entirely on UniqueID’s.

Configuration validation warns if TOC’s anchorTypes are not in UniqueID’s types:

UniqueID.configure({ types: ['heading'] }), // TOC needs heading ids
TableOfContents.configure({ anchorTypes: ['heading'] }), // matches

UniqueID does not register any commands.

UniqueID does not register any keyboard shortcuts.

UniqueID does not register any input rules.

UniqueID does not register any toolbar items.

UniqueID assigns IDs at three points:

  1. Initial load: When the editor view is ready, a setTimeout(0) dispatches a transaction that walks the entire document and assigns IDs to any configured node that lacks one
  2. New nodes: The appendTransaction hook runs after every document change. It walks the new document and assigns IDs to any node that doesn’t have one yet (new paragraphs from Enter, new list items, etc.)
  3. Pasted content: The transformPasted prop intercepts paste operations before they are applied

UniqueID uses addGlobalAttributes() to inject the ID attribute into all configured node types:

addGlobalAttributes() {
return [{
types: this.options.types,
attributes: {
[this.options.attributeName]: {
default: null,
parseHTML: (element) => element.getAttribute(this.options.attributeName),
renderHTML: (attributes) => {
const id = attributes[this.options.attributeName];
if (!id) return null;
return { [this.options.attributeName]: id };
},
},
},
}];
}

The attribute is parsed from and rendered to HTML as a standard HTML attribute (not a style):

<p id="a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d">Paragraph with unique ID</p>

When filterDuplicates is true (default), pasting content triggers the transformPasted hook:

  1. Collects all existing IDs in the current document
  2. Walks every node in the pasted slice
  3. If a pasted node’s ID already exists in the document, generates a new ID
  4. If the ID is unique, keeps it and adds it to the tracking set (to catch duplicates within the pasted content itself)

This prevents duplicate IDs when users copy-paste blocks within the same editor.

The initial assignment uses setTimeout(0) to avoid dispatching a transaction during plugin initialization. The timeout callback creates a fresh transaction from editorView.state (not a stale reference) and only dispatches if the document actually changed. The timeout is cleaned up in the plugin’s destroy() method.

The built-in generateUUID() function creates RFC 4122 v4 UUIDs without external dependencies:

function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

This uses Math.random() which is sufficient for editor IDs but not cryptographically secure. For stronger uniqueness guarantees, use crypto.randomUUID() via the generateID option.

import { UniqueID, uniqueIDPluginKey } from '@domternal/core';
import type { UniqueIDOptions } from '@domternal/core';
ExportTypeDescription
UniqueIDExtensionThe unique ID extension
uniqueIDPluginKeyPluginKeyThe ProseMirror plugin key
UniqueIDOptionsTypeScript typeOptions for UniqueID.configure()

@domternal/core - UniqueID.ts