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.
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.
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.
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.
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() {
return {
// Registers editor.commands.myCommand()
myCommand: (arg) => ({ state, dispatch }) => {
// modify state directly or use commands helpers
return true
},
}
}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() {
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.
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.
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.
Paragraph
Default block type — plain text paragraphs
Heading
h1 through h6 with configurable levels
BulletList
Unordered list with nested support
OrderedList
Numbered list with nested support
ListItem
Individual item inside bullet or ordered lists
CodeBlock
Fenced code block with language detection
Blockquote
Indented blockquote for callouts
HorizontalRule
Thematic break / divider element
Image
Image block with upload handler support
Table
Full table with drag-to-resize columns and rows
Bold
Strong text — Ctrl+B
Italic
Emphasis text — Ctrl+I
Underline
Underlined text — Ctrl+U
Strike
Strikethrough text — Ctrl+Shift+S
Code
Inline code span — Ctrl+E
Link
Hyperlinks with popover edit UI
TextColor
Per-character color via color picker
FontSize
Per-character font size override
CustomClass
Tailwind or custom CSS class on selected text
History
Undo/redo stack — Ctrl+Z / Ctrl+Shift+Z
DragAndDrop
Drag handles and block reordering
SlashCommands
/ menu for inserting blocks
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.commandsand ineditor.chain(). - --Input rules are matched in real time as the user types — use them to implement markdown-style shortcuts.