Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.superdoc.dev/llms.txt

Use this file to discover all available pages before exploring further.

Custom extensions are an advanced surface. For most app-level customization, the Document API handles document mutations and Custom UI handles toolbar, comments, and review panels — both are stable, framework-friendly, and aligned with SuperDoc’s document model. Reach for custom extensions only when you need editor internals (custom marks, nodes, or chain commands).
Create extensions to add custom features to SuperDoc.

Basic extension

import { Extensions } from 'superdoc';
const { Extension } = Extensions;

const MyExtension = Extension.create({
  name: 'myExtension',
  
  addCommands() {
    return {
      myCommand: () => ({ commands }) => {
        console.log('Command executed');
        return true;
      }
    };
  }
});

// Use it
editor.commands.myCommand();

Extension types

Node extension

For document elements:
import { Extensions } from 'superdoc';

const { Node } = Extensions;
const CustomBlock = Node.create({
  name: 'customBlock',
  group: 'block',
  content: 'inline*',
  
  parseHTML() {
    return [{ tag: 'div[data-custom]' }];
  },
  
  renderHTML({ HTMLAttributes }) {
    return ['div', { 'data-custom': '' }, 0];
  }
});

Mark extension

For inline formatting:
import { Extensions } from 'superdoc';

const { Mark } = Extensions;
const Highlight = Mark.create({
  name: 'highlight',
  
  addCommands() {
    return {
      toggleHighlight: () => ({ commands }) => {
        return commands.toggleMark(this.name);
      }
    };
  }
});

Adding features

Commands

addCommands() {
  return {
    simpleCommand: () => ({ commands }) => {
      return commands.insertContent('Hello');
    },
    
    complexCommand: (text) => ({ state, dispatch }) => {
      dispatch(state.tr.insertText(text));
      return true;
    }
  };
}

Keyboard shortcuts

addKeyboardShortcuts() {
  return {
    'Mod-Shift-h': () => this.editor.commands.toggleHighlight()
  };
}

ProseMirror plugins

Your extension can also define ProseMirror plugins which will let you perform more advanced things, such as listening to browser events attached to a node.
import { Plugin, PluginKey } from 'prosemirror-state';

// ...

addPmPlugins() {
    return [
        new Plugin({
            key: new PluginKey('myPlugin'),
            props: {
                handleClickOn: (view, pos, node, nodePos, event, direct) => {
                    console.log("Node was clicked!");
                }
            },
        })
    ];
}
You can read more about ProseMirror’s plugin system here.

Configuration

const ConfigurableExt = Extension.create({
  addOptions() {
    return {
      color: '#0000FF',
      enabled: true
    };
  },
  
  addCommands() {
    return {
      applyColor: () => () => {
        // Use this.options.color
      }
    };
  }
});

// Configure when using
ConfigurableExt.configure({ color: '#FF0000' });

Type-safe factories

Use defineNode and defineMark instead of Node.create() / Mark.create() for typed attributes:
import { defineNode, defineMark } from 'superdoc/super-editor';

const CustomBlock = defineNode({
  name: 'customBlock',
  group: 'block',
  content: 'inline*',

  addAttributes() {
    return {
      level: { default: 1 },
      collapsed: { default: false },
    };
  },

  parseHTML() {
    return [{ tag: 'div[data-custom]' }];
  },

  renderHTML({ HTMLAttributes }) {
    return ['div', { 'data-custom': '' }, 0];
  },
});

const CustomMark = defineMark({
  name: 'customMark',

  addAttributes() {
    return {
      color: { default: null },
    };
  },
});
With TypeScript generics you get full attribute autocompletion:
interface CustomBlockAttrs {
  level: number;
  collapsed: boolean;
}

const CustomBlock = defineNode<{}, {}, CustomBlockAttrs>({
  name: 'customBlock',
  // attrs are now typed as CustomBlockAttrs
});

Type guards

Use type guards when traversing the document to get typed node/mark attributes:
import { isNodeType, assertNodeType, isMarkType } from 'superdoc/super-editor';

// Type guard — narrows the type in if blocks
editor.state.doc.descendants((node) => {
  if (isNodeType(node, 'paragraph')) {
    // node.attrs is now typed as ParagraphAttrs
    console.log(node.attrs.paragraphProperties?.styleId);
  }
});

// Assertion — throws if type doesn't match
const node = state.doc.nodeAt(pos);
assertNodeType(node, 'table');
// node.attrs is now typed as TableAttrs

// Mark type guard
for (const mark of node.marks) {
  if (isMarkType(mark, 'bold')) {
    // mark.attrs is typed
  }
}

Using your extension

new SuperDoc({
  selector: '#editor',
  document: 'document.docx',
  editorExtensions: [
    MyExtension,
  ]
});