Claude Code for Tiptap Advanced: Custom Extensions and Collaboration — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Tiptap Advanced: Custom Extensions and Collaboration
Frontend

Claude Code for Tiptap Advanced: Custom Extensions and Collaboration

Published: June 15, 2027
Read time: 7 min read
By: Claude Skills 360

Tiptap advanced patterns extend beyond basic editing — Node.create({ name, group, parseHTML, renderHTML, addNodeView() { return ReactNodeViewRenderer(Component) } }) creates custom React node views. Mark.create({ name, parseHTML, renderHTML, addAttributes }) creates custom inline marks. Extension.create({ addCommands() { return { customCommand: () => ({ commands }) => commands.insertContent("x") } } }) adds editor commands. editor.commands.customCommand() invokes them. Slash commands: Suggestion({ char: "/", items: () => [...], render: () => ({ onStart, onUpdate, onKeyDown, onExit }) }) with a floating dropdown. @tiptap/extension-mention with custom render props for @mentions. Collaboration: Collaboration.configure({ document: ydoc }) + CollaborationCursor.configure({ provider }) with HocuspocusProvider. generateJSON(html, extensions) converts HTML to JSON. generateHTML(json, extensions) converts back. Claude Code generates Tiptap custom extensions, collaboration setup, AI autocomplete, and slash command menus.

CLAUDE.md for Tiptap Advanced

## Tiptap Advanced Stack
- Version: @tiptap/react >= 2.7, @tiptap/extension-* >= 2.7
- Custom node: Node.create({ name: "myNode", group: "block", addNodeView: () => ReactNodeViewRenderer(MyComponent) })
- Custom mark: Mark.create({ name: "highlight", parseHTML: () => [{ tag: "mark" }], renderHTML: ({ HTMLAttributes }) => ["mark", mergeAttributes(HTMLAttributes), 0] })
- Commands: addCommands() { return { setCustom: () => ({ commands }) => commands.setNode("custom") } }
- Suggestion: import Suggestion from "@tiptap/suggestion"; configure with char, items, render for custom menus
- Collaboration: Collaboration.configure({ document: ydoc }) — requires y-websocket or Hocuspocus server
- Export HTML: editor.getHTML(); Export JSON: editor.getJSON()
- Markdown: import { Markdown } from "tiptap-markdown"; editor.storage.markdown.getMarkdown()

Custom YouTube Embed Extension

// lib/editor/extensions/youtube.ts — custom Tiptap node extension
import { Node, mergeAttributes } from "@tiptap/core"
import { ReactNodeViewRenderer } from "@tiptap/react"
import { NodeViewWrapper } from "@tiptap/react"

// React component rendered as the node view
function YouTubeNodeView({ node, selected }: any) {
  const videoId = node.attrs.videoId
  const url = `https://www.youtube.com/embed/${videoId}`

  return (
    <NodeViewWrapper className="youtube-embed my-6" contentEditable={false}>
      <div className={`relative aspect-video rounded-xl overflow-hidden ${selected ? "ring-2 ring-primary" : ""}`}>
        <iframe
          src={url}
          className="absolute inset-0 w-full h-full"
          allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
          allowFullScreen
        />
      </div>
    </NodeViewWrapper>
  )
}

export const YouTube = Node.create({
  name: "youtube",
  group: "block",
  atom: true,

  addAttributes() {
    return {
      videoId: { default: null },
      startAt: { default: 0 },
      width: { default: "100%" },
    }
  },

  parseHTML() {
    return [
      {
        tag: 'div[data-youtube-video]',
        getAttrs: (element) => ({
          videoId: (element as HTMLElement).dataset.videoId,
        }),
      },
    ]
  },

  renderHTML({ HTMLAttributes }) {
    return ["div", mergeAttributes({ "data-youtube-video": true }, HTMLAttributes)]
  },

  addNodeView() {
    return ReactNodeViewRenderer(YouTubeNodeView)
  },

  addCommands() {
    return {
      insertYouTube: (attrs: { videoId: string }) =>
        ({ commands }) => {
          return commands.insertContent({
            type: this.name,
            attrs,
          })
        },
    } as any
  },
})

// Parse YouTube URL helper
export function extractVideoId(url: string): string | null {
  const patterns = [
    /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/,
    /youtube\.com\/embed\/([^?&\s]+)/,
  ]
  for (const pattern of patterns) {
    const match = url.match(pattern)
    if (match?.[1]) return match[1]
  }
  return null
}

Slash Commands Extension

// lib/editor/extensions/slashCommands.ts — /command slash menu
import { Extension } from "@tiptap/core"
import Suggestion from "@tiptap/suggestion"
import { ReactRenderer } from "@tiptap/react"
import tippy from "tippy.js"
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"

type CommandItem = {
  title: string
  description: string
  icon: string
  command: (editor: any) => void
}

const COMMANDS: CommandItem[] = [
  {
    title: "Heading 1",
    description: "Large section heading",
    icon: "H1",
    command: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
  },
  {
    title: "Heading 2",
    description: "Medium section heading",
    icon: "H2",
    command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
  },
  {
    title: "Bullet List",
    description: "Unordered list",
    icon: "•",
    command: (editor) => editor.chain().focus().toggleBulletList().run(),
  },
  {
    title: "Numbered List",
    description: "Ordered list",
    icon: "1.",
    command: (editor) => editor.chain().focus().toggleOrderedList().run(),
  },
  {
    title: "Quote",
    description: "Block quote",
    icon: "❝",
    command: (editor) => editor.chain().focus().toggleBlockquote().run(),
  },
  {
    title: "Code Block",
    description: "Code with syntax highlighting",
    icon: "</>",
    command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
  },
  {
    title: "Divider",
    description: "Horizontal rule",
    icon: "—",
    command: (editor) => editor.chain().focus().setHorizontalRule().run(),
  },
]

const SlashCommandList = forwardRef<any, any>(function SlashCommandList(
  { items, command }: { items: CommandItem[]; command: (item: CommandItem) => void },
  ref,
) {
  const [selectedIndex, setSelectedIndex] = useState(0)

  useImperativeHandle(ref, () => ({
    onKeyDown: ({ event }: { event: KeyboardEvent }) => {
      if (event.key === "ArrowUp") {
        setSelectedIndex((i) => (i - 1 + items.length) % items.length)
        return true
      }
      if (event.key === "ArrowDown") {
        setSelectedIndex((i) => (i + 1) % items.length)
        return true
      }
      if (event.key === "Enter") {
        command(items[selectedIndex])
        return true
      }
      return false
    },
  }))

  useEffect(() => setSelectedIndex(0), [items])

  return (
    <div className="bg-popover border rounded-xl shadow-xl p-1 w-72 overflow-hidden">
      {items.length ? (
        items.map((item, i) => (
          <button
            key={item.title}
            onClick={() => command(item)}
            className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left text-sm transition-colors ${
              i === selectedIndex ? "bg-accent text-accent-foreground" : "hover:bg-muted"
            }`}
          >
            <span className="size-8 rounded-md bg-muted flex items-center justify-center text-xs font-mono font-bold flex-shrink-0">
              {item.icon}
            </span>
            <div>
              <p className="font-medium">{item.title}</p>
              <p className="text-xs text-muted-foreground">{item.description}</p>
            </div>
          </button>
        ))
      ) : (
        <div className="px-3 py-4 text-sm text-muted-foreground text-center">No results</div>
      )}
    </div>
  )
})

export const SlashCommands = Extension.create({
  name: "slashCommands",

  addOptions() {
    return {
      suggestion: {
        char: "/",
        command: ({ editor, range, props }: any) => {
          props.command(editor)
          editor.commands.deleteRange(range)
        },
        items: ({ query }: { query: string }) => {
          return COMMANDS.filter((item) =>
            item.title.toLowerCase().startsWith(query.toLowerCase()),
          ).slice(0, 10)
        },
        // Render the floating menu
        render: () => {
          let component: ReactRenderer
          let popup: any

          return {
            onStart: (props: any) => {
              component = new ReactRenderer(SlashCommandList, {
                props,
                editor: props.editor,
              })

              popup = tippy("body", {
                getReferenceClientRect: props.clientRect,
                appendTo: () => document.body,
                content: component.element,
                showOnCreate: true,
                interactive: true,
                trigger: "manual",
                placement: "bottom-start",
              })
            },
            onUpdate: (props: any) => {
              component.updateProps(props)
              popup[0].setProps({ getReferenceClientRect: props.clientRect })
            },
            onKeyDown: ({ event }: any) => {
              if (event.key === "Escape") { popup[0].hide(); return true }
              return (component.ref as any).onKeyDown?.({ event })
            },
            onExit: () => {
              popup[0].destroy()
              component.destroy()
            },
          }
        },
      },
    }
  },

  addProseMirrorPlugins() {
    return [Suggestion({ editor: this.editor, ...this.options.suggestion })]
  },
})

Rich Text Editor Component

// components/editor/RichTextEditor.tsx — full-featured Tiptap editor
"use client"
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import Placeholder from "@tiptap/extension-placeholder"
import Link from "@tiptap/extension-link"
import Image from "@tiptap/extension-image"
import { Markdown } from "tiptap-markdown"
import Highlight from "@tiptap/extension-highlight"
import TaskList from "@tiptap/extension-task-list"
import TaskItem from "@tiptap/extension-task-item"
import CharacterCount from "@tiptap/extension-character-count"
import { SlashCommands } from "@/lib/editor/extensions/slashCommands"
import { Bold, Italic, Strikethrough, Code, Link2, Highlighter } from "lucide-react"

interface RichTextEditorProps {
  content?: string
  onChange?: (markdown: string) => void
  placeholder?: string
  maxChars?: number
}

export function RichTextEditor({
  content = "",
  onChange,
  placeholder = "Start writing... (type / for commands)",
  maxChars = 50000,
}: RichTextEditorProps) {
  const editor = useEditor({
    extensions: [
      StarterKit,
      Placeholder.configure({ placeholder }),
      Link.configure({ openOnClick: false, autolink: true }),
      Image.configure({ inline: false }),
      Highlight.configure({ multicolor: false }),
      TaskList,
      TaskItem.configure({ nested: true }),
      CharacterCount.configure({ limit: maxChars }),
      Markdown.configure({ html: false, transformPastedText: true }),
      SlashCommands,
    ],
    content,
    onUpdate: ({ editor }) => {
      onChange?.(editor.storage.markdown.getMarkdown())
    },
    editorProps: {
      attributes: {
        class: "prose prose-neutral dark:prose-invert max-w-none focus:outline-none min-h-[300px]",
      },
    },
  })

  if (!editor) return null

  const charCount = editor.storage.characterCount.characters()
  const charPercent = Math.min(100, (charCount / maxChars) * 100)

  return (
    <div className="rounded-xl border bg-card">
      {/* Inline formatting bubble menu */}
      <BubbleMenu
        editor={editor}
        tippyOptions={{ duration: 100 }}
        className="flex items-center gap-0.5 bg-popover border rounded-xl shadow-lg p-1"
      >
        {[
          { icon: <Bold size={14} />, action: () => editor.chain().focus().toggleBold().run(), active: editor.isActive("bold"), label: "Bold" },
          { icon: <Italic size={14} />, action: () => editor.chain().focus().toggleItalic().run(), active: editor.isActive("italic"), label: "Italic" },
          { icon: <Strikethrough size={14} />, action: () => editor.chain().focus().toggleStrike().run(), active: editor.isActive("strike"), label: "Strike" },
          { icon: <Code size={14} />, action: () => editor.chain().focus().toggleCode().run(), active: editor.isActive("code"), label: "Code" },
          { icon: <Highlighter size={14} />, action: () => editor.chain().focus().toggleHighlight().run(), active: editor.isActive("highlight"), label: "Highlight" },
        ].map(({ icon, action, active, label }) => (
          <button
            key={label}
            onClick={action}
            title={label}
            className={`p-1.5 rounded-lg transition-colors ${
              active ? "bg-primary text-primary-foreground" : "hover:bg-muted text-muted-foreground"
            }`}
          >
            {icon}
          </button>
        ))}
      </BubbleMenu>

      {/* Editor content */}
      <div className="p-6">
        <EditorContent editor={editor} />
      </div>

      {/* Footer */}
      <div className="flex items-center justify-between px-6 py-2 border-t text-xs text-muted-foreground">
        <span>{charCount.toLocaleString()} characters</span>
        <div className="flex items-center gap-2">
          <div className="w-16 h-1.5 bg-muted rounded-full overflow-hidden">
            <div
              className={`h-full rounded-full transition-all ${
                charPercent > 90 ? "bg-destructive" : "bg-primary"
              }`}
              style={{ width: `${charPercent}%` }}
            />
          </div>
          <span>{charPercent.toFixed(0)}%</span>
        </div>
      </div>
    </div>
  )
}

For the Lexical alternative when a React-native rich text editor from Meta with native mobile support, a more flexible plugin architecture without the “extension” coupling, and better performance for very large documents (>100k chars) is needed — Meta uses Lexical in production for Facebook and Instagram, while Tiptap has a more ergonomic DX and better documentation for common use cases, see the Lexical guide. For the Slate alternative when a fully custom editor kernel where every primitive is exposed and overridden (including the rendering pipeline) is required for highly specialized editors — Slate gives the lowest-level control while Tiptap gives the best productivity for standard rich text editing, see the Slate guide. The Claude Skills 360 bundle includes Tiptap advanced skill sets covering custom extensions, slash commands, and collaboration. Start with the free tier to try rich text editor generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free