# How to Extend the TipTap Editor with Custom Extensions in Kaneo

> Learn to extend the TipTap editor in Kaneo by creating custom extensions. Follow our guide to add new features and enhance your editor's functionality.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: how-to-guide
- Published: 2026-08-06

---

**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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/task-item-with-checkbox.tsx) | Custom node view rendering checkboxes in task lists |
| [`mention-suggestion.tsx`](https://github.com/usekaneo/kaneo/blob/main/mention-suggestion.tsx) | @-mention autocomplete using `@tiptap/suggestion` |
| [`embed-block.ts`](https://github.com/usekaneo/kaneo/blob/main/embed-block.ts) | YouTube iframe and link embed blocks |
| [`kaneo-issue-link.tsx`](https://github.com/usekaneo/kaneo/blob/main/kaneo-issue-link.tsx) | Auto-linking of issue keys to task pages |
| [`attachment-card.tsx`](https://github.com/usekaneo/kaneo/blob/main/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

```typescript
// 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

```typescript
// 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

```tsx
<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:

```typescript
// 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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/mention-suggestion.tsx)): Combines `@tiptap/suggestion` with a floating UI popover, using the editor's `queryClient` for user search.

- **EmbedBlock** ([`embed-block.ts`](https://github.com/usekaneo/kaneo/blob/main/embed-block.ts)): A pure Node extension without React—uses `renderHTML` to output iframe markup directly.

- **KaneoIssueLink** ([`kaneo-issue-link.tsx`](https://github.com/usekaneo/kaneo/blob/main/kaneo-issue-link.tsx)): Extends Link node with `parseHTML` rules that detect issue key patterns and auto-apply href attributes.

- **AttachmentCard** ([`attachment-card.tsx`](https://github.com/usekaneo/kaneo/blob/main/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:

```typescript
editor.can().setHighlight({ color: "#fdd" }) // boolean

```

## Summary

- Kaneo's TipTap 3 editor lives in **[`task-description.tsx`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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.