23 / 24
23
Extensions

Everything is an extension

Mina Editor is built on a modular extension system. Every block type, inline mark, keyboard shortcut, and input rule is an extension. You can add your own or override built-in behavior.

Extension.create()

Use Extension.create() for functional extensions that add behavior without introducing a new node type — keyboard shortcuts, input rules, or global commands.

my-extension.ts
import { Extension } from "@/components/ui/rich-editor"

const MyExtension = Extension.create({
  name: "myExtension",

  addKeyboardShortcuts() {
    return {
      "Mod-Shift-X": () => {
        // do something
        return true
      },
    }
  },

  addInputRules() {
    return [
      {
        find: /^---$/,
        handler: ({ commands }) => commands.setHorizontalRule(),
      },
    ]
  },
})

Node.create()

Use Node.create() to define a new block type with its own rendering, schema, and commands.

callout-node.ts
import { Node } from "@/components/ui/rich-editor"

const CalloutNode = Node.create({
  name: "callout",
  group: "block",
  content: "inline*",

  addAttributes() {
    return {
      type: { default: "info" }, // "info" | "warning" | "error"
    }
  },

  renderHTML({ HTMLAttributes }) {
    return ["div", { class: `callout callout-${HTMLAttributes.type}`, ...HTMLAttributes }, 0]
  },

  addCommands() {
    return {
      setCallout: (type) => ({ commands }) =>
        commands.setNode("callout", { type }),
    }
  },
})

Mark.create()

Use Mark.create() to define inline formatting that wraps a range of text — like bold, a color, or a custom highlight.

highlight-mark.ts
import { Mark } from "@/components/ui/rich-editor"

const Highlight = Mark.create({
  name: "highlight",

  addAttributes() {
    return {
      color: { default: "#FFFF00" },
    }
  },

  renderHTML({ HTMLAttributes }) {
    return ["mark", { style: `background-color: ${HTMLAttributes.color}` }, 0]
  },

  addCommands() {
    return {
      setHighlight: (color) => ({ commands }) =>
        commands.setMark("highlight", { color }),
      unsetHighlight: () => ({ commands }) =>
        commands.unsetMark("highlight"),
      toggleHighlight: (color) => ({ commands }) =>
        commands.toggleMark("highlight", { color }),
    }
  },

  addKeyboardShortcuts() {
    return {
      "Mod-Shift-H": () => this.editor.commands.toggleHighlight("#FFFF00"),
    }
  },
})

CommandManager chaining

Commands can be chained together using editor.chain(). The chain runs all commands in sequence and commits only when .run() is called.

usage.tsx
import { useEditor } from "@/components/ui/rich-editor"

function FormatButton() {
  const editor = useEditor()

  return (
    <button
      onClick={() =>
        editor
          .chain()
          .focus()
          .toggleBold()
          .toggleItalic()
          .run()
      }
    >
      Bold + Italic
    </button>
  )
}

Extension APIs

addCommands
addCommands() {
  return {
    // Registers editor.commands.myCommand()
    myCommand: (arg) => ({ state, dispatch }) => {
      // modify state directly or use commands helpers
      return true
    },
  }
}
addKeyboardShortcuts
addKeyboardShortcuts() {
  return {
    // "Mod" maps to Cmd on macOS, Ctrl on Windows/Linux
    "Mod-Alt-C": () => this.editor.commands.setCodeBlock(),
    "Shift-Enter": () => this.editor.commands.addNewline(),
  }
}
addInputRules
addInputRules() {
  return [
    // Triggered when the pattern matches at the cursor position
    {
      find: /^> $/,
      handler: ({ commands }) => commands.setBlockquote(),
    },
    {
      find: /^**([^*]+)** $/,
      handler: ({ match, commands }) =>
        commands.insertContent({ type: "text", text: match[1], marks: [{ type: "bold" }] }),
    },
  ]
}

Full example — Callout block

A complete custom block extension with attributes, rendering, commands, and a keyboard shortcut.

callout.ts
import { Node, mergeAttributes } from "@/components/ui/rich-editor"

export const Callout = Node.create({
  name: "callout",
  group: "block",
  content: "block+",
  defining: true,

  addAttributes() {
    return {
      calloutType: {
        default: "info",
        parseHTML: (el) => el.getAttribute("data-type"),
        renderHTML: (attrs) => ({ "data-type": attrs.calloutType }),
      },
    }
  },

  parseHTML() {
    return [{ tag: "div[data-callout]" }]
  },

  renderHTML({ HTMLAttributes }) {
    return [
      "div",
      mergeAttributes(HTMLAttributes, { "data-callout": "", class: "callout" }),
      0,
    ]
  },

  addCommands() {
    return {
      setCallout:
        (calloutType = "info") =>
        ({ commands }) =>
          commands.wrapIn(this.name, { calloutType }),

      unsetCallout:
        () =>
        ({ commands }) =>
          commands.lift(this.name),
    }
  },

  addKeyboardShortcuts() {
    return {
      "Mod-Shift-C": () => this.editor.commands.setCallout("info"),
    }
  },
})

// Register with EditorProvider:
// <EditorProvider extensions={[Callout]} ...>

Full example — Highlight mark

A complete custom inline mark extension with color attribute, toggle command, and keyboard shortcut.

highlight.ts
import { Mark, mergeAttributes } from "@/components/ui/rich-editor"

export const Highlight = Mark.create({
  name: "highlight",

  addOptions() {
    return {
      defaultColor: "#FFF176",
    }
  },

  addAttributes() {
    return {
      color: {
        default: null,
        parseHTML: (el) => el.style.backgroundColor || null,
        renderHTML: (attrs) =>
          attrs.color ? { style: `background-color: ${attrs.color}` } : {},
      },
    }
  },

  parseHTML() {
    return [{ tag: "mark" }]
  },

  renderHTML({ HTMLAttributes }) {
    return ["mark", mergeAttributes(HTMLAttributes), 0]
  },

  addCommands() {
    return {
      setHighlight:
        (color) =>
        ({ commands }) =>
          commands.setMark(this.name, { color: color ?? this.options.defaultColor }),

      unsetHighlight:
        () =>
        ({ commands }) =>
          commands.unsetMark(this.name),

      toggleHighlight:
        (color) =>
        ({ commands }) =>
          commands.toggleMark(this.name, { color: color ?? this.options.defaultColor }),
    }
  },

  addKeyboardShortcuts() {
    return {
      "Mod-Shift-H": () =>
        this.editor.commands.toggleHighlight(this.options.defaultColor),
    }
  },
})

// Usage: editor.chain().focus().toggleHighlight("#FFFF00").run()

StarterKit — built-in extensions

StarterKit is a convenience bundle that includes all 22 built-in extensions. Passing your own extensions prop merges with the starter kit.

01block

Paragraph

Default block type — plain text paragraphs

02block

Heading

h1 through h6 with configurable levels

03block

BulletList

Unordered list with nested support

04block

OrderedList

Numbered list with nested support

05block

ListItem

Individual item inside bullet or ordered lists

06block

CodeBlock

Fenced code block with language detection

07block

Blockquote

Indented blockquote for callouts

08block

HorizontalRule

Thematic break / divider element

09block

Image

Image block with upload handler support

10block

Table

Full table with drag-to-resize columns and rows

11inline

Bold

Strong text — Ctrl+B

12inline

Italic

Emphasis text — Ctrl+I

13inline

Underline

Underlined text — Ctrl+U

14inline

Strike

Strikethrough text — Ctrl+Shift+S

15inline

Code

Inline code span — Ctrl+E

16inline

Link

Hyperlinks with popover edit UI

17inline

TextColor

Per-character color via color picker

18inline

FontSize

Per-character font size override

19inline

CustomClass

Tailwind or custom CSS class on selected text

20feature

History

Undo/redo stack — Ctrl+Z / Ctrl+Shift+Z

21feature

DragAndDrop

Drag handles and block reordering

22feature

SlashCommands

/ menu for inserting blocks

usage.tsx
import { EditorProvider, StarterKit } from "@/components/ui/rich-editor"
import { Callout } from "./callout"
import { Highlight } from "./highlight"

// Extends StarterKit with your custom extensions
<EditorProvider
  extensions={[
    ...StarterKit,
    Callout,
    Highlight,
  ]}
  initialState={initialState}
>
  <Editor />
</EditorProvider>
  • --Extensions are plain objects — they are tree-shakeable and have zero runtime overhead when unused.
  • --Node.create() for block-level elements, Mark.create() for inline ranges, Extension.create() for behavior-only additions.
  • --Commands added via addCommands() are automatically available on editor.commands and in editor.chain().
  • --Input rules are matched in real time as the user types — use them to implement markdown-style shortcuts.
Documentation | Mina Rich Editor