How to Extend the TipTap Editor with Custom Extensions in Kaneo
To extend the TipTap editor in Kaneo, create a new extension in apps/web/src/components/task/extensions/, export it, import it into task-description.tsx, and add it to the extensions array passed to useEditor.
Kaneo ships with a powerful TipTap 3 editor embedded in its task detail view. The editor is built around a modular extension system that allows you to add custom nodes, marks, and behaviors without touching core editor logic. This guide walks through the exact architecture used in the usekaneo/kaneo repository and demonstrates how to implement your own extensions.
How Kaneo Structures TipTap Extensions
In Kaneo, all editor extensions live in apps/web/src/components/task/extensions/. The project follows a consistent pattern where each capability is isolated in its own file and registered through the useEditor hook.
Core Extension Files
| File | Purpose |
|---|---|
task-item-with-checkbox.tsx |
Custom node view rendering checkboxes in task lists |
mention-suggestion.tsx |
@-mention autocomplete using @tiptap/suggestion |
embed-block.ts |
YouTube iframe and link embed blocks |
kaneo-issue-link.tsx |
Auto-linking of issue keys to task pages |
attachment-card.tsx |
Interactive file attachment cards |
Each extension exports either a Node.create(), Mark.create(), or Extension.create() instance that conforms to the TipTap API.
Creating a Custom Mark Extension
A mark extension adds inline formatting capabilities. Here's a complete highlight mark that lets users apply background colors to text.
Step 1: Define the Extension
// apps/web/src/components/task/extensions/highlight.ts
import { Mark, mergeAttributes } from "@tiptap/core";
export const Highlight = Mark.create({
name: "highlight",
addAttributes() {
return {
color: { default: "#ff0" },
};
},
parseHTML() {
return [{ tag: "span[data-highlight]" }];
},
renderHTML({ HTMLAttributes }) {
return [
"span",
mergeAttributes(HTMLAttributes, {
"data-highlight": "",
style: `background-color:${HTMLAttributes.color}`,
}),
0,
];
},
addCommands() {
return {
setHighlight:
(attrs) =>
({ chain }) => {
return chain().setMark(this.name, attrs).run();
},
};
},
});
The addCommands() method exposes setHighlight() to the editor's command chain, which you'll trigger from UI controls.
Step 2: Register in the Editor
// apps/web/src/components/task/task-description.tsx
import { Highlight } from "./extensions/highlight";
export const TaskDescriptionEditor = ({ taskId }: TaskDescriptionProps) => {
const editor = useEditor({
extensions: [
StarterKit,
TaskList,
TaskItemWithCheckbox,
MentionSuggestion,
EmbedBlock,
KaneoIssueLink,
AttachmentCard,
Highlight, // ← your new extension
],
// editor configuration...
});
// ...
};
The extensions array order matters for priority—place mark extensions after core nodes but before any that depend on them.
Step 3: Wire to Toolbar UI
<Button
onClick={() => editor?.chain().focus().setHighlight({ color: "#fdd" }).run()}
>
Highlight
</Button>
The chain().focus() pattern ensures proper selection handling before applying the mark.
Creating a Custom Node View with React
For block-level elements that need React rendering, use ReactNodeViewRenderer. This example creates a titled container that editors can insert:
// apps/web/src/components/task/extensions/custom-node.tsx
import { Node } from "@tiptap/core";
import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
function MyNodeView({ node }: NodeViewProps) {
return (
<NodeViewWrapper className="my-node">
<strong>{node.attrs.title}</strong>
</NodeViewWrapper>
);
}
export const MyNode = Node.create({
name: "myNode",
group: "block",
addAttributes() {
return {
title: { default: "" }
};
},
addNodeView() {
return ReactNodeViewRenderer(MyNodeView);
},
});
Register this node the same way as the Highlight mark—import and add to the extensions array. The NodeViewWrapper component handles selection, drag-and-drop, and prosemirror integration automatically.
Extension Patterns in Kaneo's Source
Studying existing extensions reveals consistent implementation strategies:
-
TaskItemWithCheckbox (
task-item-with-checkbox.tsx): Extends the base TaskItem with a customaddNodeView()that renders a controlled checkbox input synchronized to document state. -
MentionSuggestion (
mention-suggestion.tsx): Combines@tiptap/suggestionwith a floating UI popover, using the editor'squeryClientfor user search. -
EmbedBlock (
embed-block.ts): A pure Node extension without React—usesrenderHTMLto output iframe markup directly. -
KaneoIssueLink (
kaneo-issue-link.tsx): Extends Link node withparseHTMLrules that detect issue key patterns and auto-apply href attributes. -
AttachmentCard (
attachment-card.tsx): Complex node view with drag handles, delete buttons, and file metadata rendering via ReactNodeViewRenderer.
Best Practices for Custom Extensions
-
Keep extensions self-contained: Each file should export one extension and handle its own types, commands, and keyboard shortcuts.
-
Use TypeScript generics for attributes: Declare attribute interfaces to get type safety on
node.attrsin React components. -
Leverage
addKeyboardShortcuts(): Bind common actions to keys—Kaneo's TaskItemWithCheckbox uses this forCtrl+Enterto toggle completion. -
Test commands with
editor.can(): Before showing toolbar buttons, verify the command is executable in current context:
editor.can().setHighlight({ color: "#fdd" }) // boolean
Summary
-
Kaneo's TipTap 3 editor lives in
task-description.tsxand accepts extensions through theuseEditorhook'sextensionsarray. -
All first-party extensions reside in
apps/web/src/components/task/extensions/and follow TipTap'sNode.create(),Mark.create(), orExtension.create()patterns. -
To extend the editor: create your extension file, export the configuration, import it into
task-description.tsx, and append it to the extensions array. -
Use ReactNodeViewRenderer for React-based node views and pure
renderHTMLfor static output. -
Study
TaskItemWithCheckbox,MentionSuggestion, andAttachmentCardfor production-ready implementation patterns.
Frequently Asked Questions
Can I use third-party TipTap extensions in Kaneo?
Yes. Install the package with your package manager, import the extension, and add it to the extensions array in task-description.tsx. Kaneo's setup is standard TipTap 3—no custom wrappers required. Ensure version compatibility with @tiptap/core@^3.x.
How do I add keyboard shortcuts to my custom extension?
Implement the addKeyboardShortcuts() method in your extension configuration. Return an object mapping key combinations to command functions. Refer to task-item-with-checkbox.tsx for an example using Mod-Enter to toggle task completion state.
What's the difference between Node, Mark, and Extension in TipTap?
Nodes define block-level elements (paragraphs, headings, custom cards). Marks apply inline formatting that spans text (bold, highlight, links). Extensions add behavior without new document structures (keymaps, input rules, global plugins). Choose based on whether you're adding structure, formatting, or functionality.
How do I access editor state outside the TaskDescription component?
Use TipTap's useEditor hook or create a standalone editor instance with new Editor(). For cross-component access, consider using a React context provider that wraps the editor instance, or use TipTap's built-in event system with editor.on('update', callback) for reactive updates.
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 →