Claude Code for Tiptap: Rich Text Editor with Extensions — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Tiptap: Rich Text Editor with Extensions
Frontend

Claude Code for Tiptap: Rich Text Editor with Extensions

Published: March 4, 2027
Read time: 8 min read
By: Claude Skills 360

Tiptap is a headless rich text editor built on ProseMirror — it provides the editing logic without any UI, letting you style everything with Tailwind or CSS. useEditor({ extensions, content }) initializes the editor in React. StarterKit bundles Heading, Bold, Italic, Code, Blockquote, BulletList, and Paragraph. Custom Node extensions add new block types like callouts or image captions; Mark extensions add inline styles. The suggestion extension powers slash commands — a command palette triggered by /. @tiptap/extension-collaboration and @hocuspocus/provider add real-time multi-user editing via Y.js CRDTs. BubbleMenu renders a floating formatting toolbar when text is selected. FloatingMenu appears on empty lines for block-level commands. editor.getJSON() serializes to JSON; editor.getHTML() to HTML. Claude Code generates Tiptap editor configurations, custom extensions, slash command implementations, and the collaboration setup for document editing applications.

CLAUDE.md for Tiptap

## Tiptap Stack
- Version: @tiptap/react >= 2.10, @tiptap/starter-kit >= 2.10
- Init: useEditor({ extensions: [...], content: initialContent, onUpdate: ({ editor }) => {...} })
- StarterKit: Heading/Bold/Italic/Code/Blockquote/BulletList/OrderedList/HardBreak/History
- Custom Node: Node.create({ name, group, content, parseHTML, renderHTML, addNodeView })
- Custom Mark: Mark.create({ name, parseHTML, renderHTML, addAttributes })
- Slash: Suggestion extension + renderItems callback — command palette
- Collab: Collaboration (Y.js) + CollaborationCursor + HocuspocusProvider
- Output: editor.getJSON() | editor.getHTML() | editor.getText()

Editor Setup

// components/editor/RichTextEditor.tsx — main editor component
"use client"
import { useEditor, EditorContent, BubbleMenu, FloatingMenu } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import { Link } from "@tiptap/extension-link"
import { Image } from "@tiptap/extension-image"
import { Placeholder } from "@tiptap/extension-placeholder"
import { CharacterCount } from "@tiptap/extension-character-count"
import { SlashCommands } from "./extensions/SlashCommands"
import { Callout } from "./extensions/Callout"
import { EditorToolbar } from "./EditorToolbar"
import { BubbleToolbar } from "./BubbleToolbar"
import type { JSONContent } from "@tiptap/core"

interface RichTextEditorProps {
  content?: JSONContent
  onChange?: (content: JSONContent) => void
  onBlur?: () => void
  placeholder?: string
  readOnly?: boolean
  maxLength?: number
}

export function RichTextEditor({
  content,
  onChange,
  onBlur,
  placeholder = "Write something...",
  readOnly = false,
  maxLength,
}: RichTextEditorProps) {
  const editor = useEditor({
    extensions: [
      StarterKit.configure({
        heading: { levels: [1, 2, 3] },
        code: { HTMLAttributes: { class: "rounded bg-muted px-1 py-0.5 font-mono text-sm" } },
        codeBlock: false,  // Using custom CodeBlock extension instead
      }),
      Placeholder.configure({ placeholder }),
      Link.configure({
        openOnClick: false,
        HTMLAttributes: { class: "text-blue-600 underline" },
      }),
      Image.configure({
        HTMLAttributes: { class: "max-w-full rounded-lg my-4" },
      }),
      CharacterCount.configure({ limit: maxLength }),
      SlashCommands,
      Callout,
    ],
    content,
    editable: !readOnly,
    onUpdate: ({ editor }) => {
      onChange?.(editor.getJSON())
    },
    onBlur: () => onBlur?.(),
    editorProps: {
      attributes: {
        class: "prose prose-stone max-w-none focus:outline-none min-h-[200px] px-4 py-3",
      },
    },
  })

  if (!editor) return null

  return (
    <div className="border rounded-lg overflow-hidden">
      {!readOnly && <EditorToolbar editor={editor} />}

      <BubbleMenu editor={editor} tippyOptions={{ duration: 100 }}>
        <BubbleToolbar editor={editor} />
      </BubbleMenu>

      <FloatingMenu editor={editor} tippyOptions={{ duration: 100 }}>
        <div className="text-muted-foreground text-sm px-2">
          Type / for commands
        </div>
      </FloatingMenu>

      <EditorContent editor={editor} />

      {maxLength && (
        <div className="flex justify-end px-4 py-2 text-xs text-muted-foreground border-t">
          {editor.storage.characterCount.characters()} / {maxLength}
        </div>
      )}
    </div>
  )
}

Custom Node Extension

// components/editor/extensions/Callout.ts — custom block extension
import { Node, mergeAttributes } from "@tiptap/core"
import { ReactNodeViewRenderer } from "@tiptap/react"
import { CalloutView } from "./CalloutView"

export type CalloutType = "info" | "warning" | "success" | "error"

declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    callout: {
      setCallout: (attrs: { type: CalloutType }) => ReturnType
    }
  }
}

export const Callout = Node.create({
  name: "callout",
  group: "block",
  content: "inline*",
  defining: true,

  addAttributes() {
    return {
      type: {
        default: "info",
        parseHTML: element => element.getAttribute("data-type"),
        renderHTML: attributes => ({ "data-type": attributes.type }),
      },
    }
  },

  parseHTML() {
    return [{ tag: "div[data-callout]" }]
  },

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

  addNodeView() {
    return ReactNodeViewRenderer(CalloutView)
  },

  addCommands() {
    return {
      setCallout:
        attrs =>
        ({ commands }) => {
          return commands.insertContent({
            type: this.name,
            attrs,
            content: [{ type: "text", text: "" }],
          })
        },
    }
  },
})

Slash Commands Extension

// components/editor/extensions/SlashCommands.ts
import { Extension } from "@tiptap/core"
import Suggestion from "@tiptap/suggestion"
import { ReactRenderer } from "@tiptap/react"
import tippy from "tippy.js"
import { CommandsList } from "./CommandsList"

const slashCommands = [
  {
    title: "Heading 1",
    description: "Large section heading",
    icon: "H1",
    command: ({ editor, range }: any) => {
      editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run()
    },
  },
  {
    title: "Heading 2",
    description: "Medium section heading",
    icon: "H2",
    command: ({ editor, range }: any) => {
      editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run()
    },
  },
  {
    title: "Bullet List",
    description: "Create a simple bullet list",
    icon: "•",
    command: ({ editor, range }: any) => {
      editor.chain().focus().deleteRange(range).toggleBulletList().run()
    },
  },
  {
    title: "Callout",
    description: "Highlighted information block",
    icon: "💡",
    command: ({ editor, range }: any) => {
      editor.chain().focus().deleteRange(range).setCallout({ type: "info" }).run()
    },
  },
]

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

  addOptions() {
    return {
      suggestion: {
        char: "/",
        command: ({ editor, range, props }: any) => {
          props.command({ editor, range })
        },
        items: ({ query }: { query: string }) =>
          slashCommands
            .filter(cmd =>
              cmd.title.toLowerCase().includes(query.toLowerCase())
            )
            .slice(0, 10),

        render: () => {
          let component: ReactRenderer
          let popup: ReturnType<typeof tippy>

          return {
            onStart: (props: any) => {
              component = new ReactRenderer(CommandsList, {
                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: (props: any) => {
              if (props.event.key === "Escape") {
                popup?.[0]?.hide()
                return true
              }
              return (component?.ref as any)?.onKeyDown?.(props)
            },
            onExit: () => {
              popup?.[0]?.destroy()
              component?.destroy()
            },
          }
        },
      },
    }
  },

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

Collaboration with Hocuspocus

// components/editor/CollaborativeEditor.tsx — real-time collaboration
"use client"
import { useEditor, EditorContent } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import Collaboration from "@tiptap/extension-collaboration"
import CollaborationCursor from "@tiptap/extension-collaboration-cursor"
import { HocuspocusProvider } from "@hocuspocus/provider"
import * as Y from "yjs"
import { useEffect, useRef } from "react"
import { useSession } from "@/lib/auth"

export function CollaborativeEditor({ documentId }: { documentId: string }) {
  const { user } = useSession()
  const ydoc = useRef(new Y.Doc()).current

  const provider = useRef(
    new HocuspocusProvider({
      url: process.env.NEXT_PUBLIC_HOCUSPOCUS_URL!,
      name: documentId,
      document: ydoc,
      token: () => fetch("/api/hocuspocus-token").then(r => r.text()),
    })
  ).current

  const editor = useEditor({
    extensions: [
      StarterKit.configure({ history: false }),  // Disabled — Y.js handles undo
      Collaboration.configure({ document: ydoc }),
      CollaborationCursor.configure({
        provider,
        user: {
          name: user?.name ?? "Anonymous",
          color: `#${Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0")}`,
        },
      }),
    ],
  })

  useEffect(() => {
    return () => {
      provider.destroy()
      ydoc.destroy()
    }
  }, [])

  return (
    <div className="relative">
      <div className="flex gap-2 p-2 border-b">
        {Array.from((editor?.storage.collaborationCursor.users ?? []) as any[]).map((u: any) => (
          <div
            key={u.clientId}
            className="h-7 w-7 rounded-full flex items-center justify-center text-xs font-medium text-white"
            style={{ backgroundColor: u.color }}
            title={u.name}
          >
            {u.name?.[0]?.toUpperCase()}
          </div>
        ))}
      </div>
      <EditorContent editor={editor} />
    </div>
  )
}

For the Lexical editor alternative from Meta that provides a similar extensible editor framework with a different node-based architecture, better performance on very large documents, and native support for React 18 concurrent features, the React patterns guide covers editor integration. For the Quill.js lightweight editor alternative when a simpler editing experience without ProseMirror’s complexity and a maturer browser compatibility story for legacy applications is needed, consider the basic editor patterns. The Claude Skills 360 bundle includes Tiptap skill sets covering extensions, slash commands, and collaboration. Start with the free tier to try Tiptap editor configuration 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