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 custom addNodeView() that renders a controlled checkbox input synchronized to document state.

  • MentionSuggestion (mention-suggestion.tsx): Combines @tiptap/suggestion with a floating UI popover, using the editor's queryClient for user search.

  • EmbedBlock (embed-block.ts): A pure Node extension without React—uses renderHTML to output iframe markup directly.

  • KaneoIssueLink (kaneo-issue-link.tsx): Extends Link node with parseHTML rules 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.attrs in React components.

  • Leverage addKeyboardShortcuts(): Bind common actions to keys—Kaneo's TaskItemWithCheckbox uses this for Ctrl+Enter to 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.tsx and accepts extensions through the useEditor hook's extensions array.

  • All first-party extensions reside in apps/web/src/components/task/extensions/ and follow TipTap's Node.create(), Mark.create(), or Extension.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 renderHTML for static output.

  • Study TaskItemWithCheckbox, MentionSuggestion, and AttachmentCard for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →