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 { Editor, Document, Paragraph, Text, UniqueID } from '@domternal/core';
import '@domternal/theme';
const editor = new Editor({
element: 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
})

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