How to Extend Tiptap Rich Text Editor with Custom Extensions in Plane
You extend Tiptap in Plane by creating a standard Tiptap extension (Mark, Node, or Extension), exporting it from the editor package, and registering it in the extensionRegistry array within rich-text-extensions.tsx, where the isEnabled predicate controls visibility.
Plane's rich-text editor is built on Tiptap v2 and uses a centralized extension registry to manage optional features. The codebase in the makeplane/plane repository provides a structured way to add custom marks, nodes, and commands without modifying core editor logic.
Understanding Plane's Extension Architecture
Plane's editor architecture separates core functionality from optional extensions through a registry pattern. The main editor component in packages/editor/src/core/components/editors/rich-text/editor.tsx initializes the Tiptap instance using a getExtensions callback that aggregates extensions from multiple sources.
Optional extensions are managed by RichTextEditorAdditionalExtensions in packages/editor/src/ce/extensions/rich-text-extensions.tsx. This utility reads from an extensionRegistry array that determines which extensions to load based on isEnabled predicates and instantiates them via getExtension factories.
This design allows extensions to be conditionally enabled or disabled through props like disabledExtensions and flaggedExtensions, making it possible to toggle features per workspace or user preference.
Step-by-Step Guide to Extending Tiptap
Step 1: Create a Custom Tiptap Extension
Create your extension file following Tiptap v2 conventions. Plane supports standard Mark, Node, and Extension types.
// packages/editor/src/core/extensions/custom-highlight.ts
import { Mark, mergeAttributes } from "@tiptap/core";
export const Highlight = Mark.create({
name: "highlight",
addOptions() {
return { HTMLAttributes: { class: "bg-yellow-200" } };
},
parseHTML() {
return [{ tag: "mark" }];
},
renderHTML({ HTMLAttributes }) {
return ["mark", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
addCommands() {
return {
toggleHighlight:
() =>
({ commands }) => {
return commands.toggleMark(this.name);
},
};
},
});
Step 2: Export the Extension from the Package Index
Update the barrel file to make your extension available to the registry.
// packages/editor/src/core/extensions/index.ts
export * from "./custom-highlight";
Now the extension can be imported elsewhere using import { Highlight } from "@/extensions" (the @ alias points to packages/editor/src).
Step 3: Register in the Extension Registry
Add a new entry to extensionRegistry in rich-text-extensions.tsx. The isEnabled function checks against disabled extensions, while getExtension returns the extension class.
// packages/editor/src/ce/extensions/rich-text-extensions.tsx
import { Highlight } from "@/extensions/custom-highlight";
const extensionRegistry: TRichTextEditorAdditionalExtensionsRegistry[] = [
// Existing entries...
{
isEnabled: (disabledExtensions) => !disabledExtensions.includes("highlight"),
getExtension: () => Highlight,
},
];
Step 4: Implement UI Controls
Access your custom commands through the editor context. Plane's useEditorContext hook provides access to the Tiptap editor instance.
// Example: a toolbar button inside a custom editor UI
import { useEditorContext } from "@/hooks";
export function HighlightButton() {
const { editor } = useEditorContext();
return (
<button
onClick={() => editor?.chain().focus().toggleHighlight().run()}
className="p-2"
>
Highlight
</button>
);
}
When the button is clicked, the toggleHighlight command defined in the extension runs, wrapping the selection in a <mark> element styled by the CSS class defined in addOptions.
Step 5: Conditionally Disable Extensions
Control extension availability by passing the extension name to the disabledExtensions prop.
<RichTextEditorWithRef
disabledExtensions={["highlight"]} // hides the Highlight extension
{...otherProps}
/>
Key Implementation Files
- Core Editor Component (
packages/editor/src/core/components/editors/rich-text/editor.tsx): Creates the Tiptap editor instance and callsgetExtensionsto assemble the extension list. - Extension Registry (
packages/editor/src/ce/extensions/rich-text-extensions.tsx): Contains theextensionRegistryarray andRichTextEditorAdditionalExtensionslogic that filters extensions based onisEnabledpredicates. - Export Barrel (
packages/editor/src/core/extensions/index.ts): Central export point for custom extensions within the editor package. - Slash Commands Example (
packages/editor/src/extensions/slash-commands/root.tsx): Reference implementation of an optional extension using the registry pattern.
Summary
- Plane uses an
extensionRegistrypattern inpackages/editor/src/ce/extensions/rich-text-extensions.tsxto manage optional Tiptap extensions. - Custom extensions must implement standard Tiptap v2 APIs (Mark, Node, or Extension) and be exported from
packages/editor/src/core/extensions/index.ts. - Register extensions with
isEnabledandgetExtensionproperties to control initialization logic. - Control visibility via the
disabledExtensionsprop on the editor component. - UI commands integrate through the
useEditorContexthook and standard Tiptap command chains likeeditor.chain().focus().toggleHighlight().run().
Frequently Asked Questions
What is the extension registry in Plane?
The extension registry is an array of configuration objects defined in packages/editor/src/ce/extensions/rich-text-extensions.tsx. Each entry contains an isEnabled function that determines if the extension should load based on current props, and a getExtension factory that returns the Tiptap extension instance. This pattern decouples extension availability from the core editor initialization in editor.tsx.
How do I disable a custom extension for specific users?
Pass the extension name to the disabledExtensions array prop on the RichTextEditorWithRef component. The isEnabled predicate in the registry checks against this array, preventing the extension from being included in the editor's extension list for that specific instance.
Can I add custom nodes as well as marks?
Yes. Plane's registry supports all Tiptap v2 extension types including Node, Mark, and plain Extension. The registration process remains identical regardless of the extension type—create the extension file, export it from the index, and add it to the extensionRegistry with appropriate isEnabled logic.
Where should I store custom extension files?
Store custom extensions in packages/editor/src/core/extensions/ or a subdirectory thereof. Import them into packages/editor/src/core/extensions/index.ts to maintain the package's export structure, then reference them in packages/editor/src/ce/extensions/rich-text-extensions.tsx using the @/extensions alias.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →