Build rich text editors with Tiptap - headless editor framework with React, shadcn/ui, and Tailwind v4 integration. Includes SSR-safe setup, image uploads to R2, prose styling, collaborative editing, and markdown support. Use when creating blog editors, comment systems, documentation platforms, or Notion-like apps, or troubleshooting SSR hydration errors, Tailwind typography issues, or image upload performance.
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
README.mdreferences/common-errors.mdreferences/extension-catalog.mdreferences/tiptap-docs.mdtemplates/base-editor.tsxtemplates/common-extensions.tstemplates/image-upload-r2.tsxtemplates/minimal-tiptap-setup.shtemplates/package.jsontemplates/tiptap-prose.cssname: tiptap description: | Build rich text editors with Tiptap - headless editor framework with React, shadcn/ui, and Tailwind v4 integration. Includes SSR-safe setup, image uploads to R2, prose styling, collaborative editing, and markdown support.
Status: Production Ready Last Updated: 2025-11-29 Dependencies: React 19+, Tailwind v4, shadcn/ui (recommended) Latest Versions: @tiptap/react@3.11.1, @tiptap/starter-kit@3.11.1, @tiptap/pm@3.11.1
npm install @tiptap/react @tiptap/starter-kit @tiptap/pm @tiptap/extension-image @tiptap/extension-color @tiptap/extension-text-style @tiptap/extension-typography
Why this matters:
@tiptap/pm is required peer dependency (ProseMirror engine)import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
export function Editor() {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
immediatelyRender: false, // ⚠️ CRITICAL for SSR/Next.js
editorProps: {
attributes: {
class: 'prose prose-sm focus:outline-none min-h-[200px] p-4',
},
},
})
return <EditorContent editor={editor} />
}
CRITICAL:
immediatelyRender: false for Next.js/SSR apps (prevents hydration mismatch)immediatelyRender explicitly to false"npm install @tailwindcss/typography
Update your tailwind.config.ts:
import typography from '@tailwindcss/typography'
export default {
plugins: [typography],
}
Why this matters:
.tiptap selectorOption A: shadcn Minimal Tiptap Component (Recommended)
Install the pre-built shadcn component:
npx shadcn@latest add https://raw.githubusercontent.com/Aslam97/shadcn-minimal-tiptap/main/registry/block-registry.json
This installs:
Option B: Build Custom Editor (Full Control)
Use templates from this skill:
templates/base-editor.tsx - Minimal editor setuptemplates/common-extensions.ts - Extension bundletemplates/tiptap-prose.css - Tailwind stylingKey Points:
Extensions add functionality to your editor:
import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image'
import Link from '@tiptap/extension-link'
import Typography from '@tiptap/extension-typography'
const editor = useEditor({
extensions: [
StarterKit.configure({
// Customize built-in extensions
heading: {
levels: [1, 2, 3],
},
bulletList: {
keepMarks: true,
},
}),
Image.configure({
inline: true,
allowBase64: false, // ⚠️ Prevent base64 bloat
resize: {
enabled: true,
directions: ['top-right', 'bottom-right', 'bottom-left', 'top-left'],
minWidth: 100,
minHeight: 100,
alwaysPreserveAspectRatio: true,
},
}),
Link.configure({
openOnClick: false,
HTMLAttributes: {
class: 'text-primary underline',
},
}),
Typography, // Smart quotes, dashes, etc.
],
})
CRITICAL:
allowBase64: false to prevent huge JSON payloadsPattern: Base64 preview → background upload → replace with URL
See templates/image-upload-r2.tsx for full implementation:
import { Editor } from '@tiptap/core'
async function uploadImageToR2(file: File, env: Env): Promise<string> {
// 1. Create base64 preview for immediate display
const reader = new FileReader()
const base64 = await new Promise<string>((resolve) => {
reader.onload = () => resolve(reader.result as string)
reader.readAsDataURL(file)
})
// 2. Insert preview into editor
editor.chain().focus().setImage({ src: base64 }).run()
// 3. Upload to R2 in background
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const { url } = await response.json()
// 4. Replace base64 with permanent URL
editor.chain()
.focus()
.updateAttributes('image', { src: url })
.run()
return url
}
Why this pattern:
✅ Set immediatelyRender: false in useEditor() for SSR apps
✅ Install @tailwindcss/typography for prose styling
✅ Use upload handler for images (not base64)
✅ Memoize editor configuration to prevent re-renders
✅ Include @tiptap/pm peer dependency
❌ Use immediatelyRender: true (default) with Next.js/SSR
❌ Store images as base64 in database (use URL after upload)
❌ Forget to add prose classes to editor container
❌ Load more than 100 widgets in collaborative mode
❌ Use Create React App (v3 incompatible - use Vite)
This skill prevents 5 documented issues:
Error: "SSR has been detected, please set immediatelyRender explicitly to false"
Source: GitHub Issue #5856, #5602
Why It Happens: Default immediatelyRender: true breaks Next.js hydration
Prevention: Template includes immediatelyRender: false by default
Error: Laggy typing, poor performance in large documents
Source: Tiptap Performance Docs
Why It Happens: useEditor() hook re-renders component on every change
Prevention: Use useEditorState() hook or memoization patterns (see templates)
Error: Headings/lists render unstyled, no formatting visible
Source: shadcn Tiptap Discussion
Why It Happens: Missing @tailwindcss/typography plugin
Prevention: Skill includes typography plugin installation in checklist
Error: JSON payloads become megabytes, slow saves, database bloat Source: Tiptap Image Docs Why It Happens: Default allows base64, no upload handler configured Prevention: R2 upload template with URL replacement pattern
Error: "jsx-runtime" module resolution errors after upgrading to v3 Source: GitHub Issue #6812 Why It Happens: CRA incompatibility with v3 module structure Prevention: Skill documents Vite as preferred bundler + provides working config
/* Apply to editor container */
.tiptap {
/* Tailwind Typography */
@apply prose prose-sm sm:prose-base lg:prose-lg dark:prose-invert max-w-none;
/* Custom overrides */
h1 {
@apply text-3xl font-bold mt-8 mb-4;
}
h2 {
@apply text-2xl font-semibold mt-6 mb-3;
}
p {
@apply my-4 text-base leading-7;
}
ul, ol {
@apply my-4 ml-6;
}
code {
@apply bg-muted px-1.5 py-0.5 rounded text-sm font-mono;
}
pre {
@apply bg-muted p-4 rounded-lg overflow-x-auto;
}
blockquote {
@apply border-l-4 border-primary pl-4 italic my-4;
}
}
Why these settings:
prose classes provide consistent formattingdark:prose-invert handles dark mode automaticallyimport { useEditor } from '@tiptap/react'
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'
const ydoc = new Y.Doc()
const editor = useEditor({
extensions: [
StarterKit.configure({
history: false, // Disable history for collaboration
}),
Collaboration.configure({
document: ydoc,
}),
],
})
When to use: Real-time multi-user editing (Notion-like)
See: templates/collaborative-setup.tsx for full example
import { useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'
// Load editor with markdown content
const editor = useEditor({
extensions: [StarterKit, Markdown],
content: '# Hello World\n\nThis is **Markdown**!',
contentType: 'markdown', // ⚠️ CRITICAL: Must specify or content parsed as HTML
immediatelyRender: false,
})
// Get markdown from editor
const markdownOutput = editor.getMarkdown()
// Insert markdown content
editor.commands.setContent('## New heading', { contentType: 'markdown' })
editor.commands.insertContent('**Bold** text', { contentType: 'markdown' })
When to use: Storing content as markdown, displaying/editing rich text
Install: npm install @tiptap/markdown@3.11.1
Status: Beta (released Oct 2025, API stable but may change)
CRITICAL: Always specify contentType: 'markdown' when setting markdown content
import { useForm, Controller } from 'react-hook-form'
function BlogForm() {
const { control, handleSubmit } = useForm()
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="content"
control={control}
render={({ field }) => (
<Editor
content={field.value}
onUpdate={({ editor }) => {
field.onChange(editor.getHTML())
}}
/>
)}
/>
</form>
)
}
When to use: Blog posts, comments, any form-based content
No executable scripts for this skill.
Required for all projects:
templates/base-editor.tsx - Minimal React editor componenttemplates/package.json - Required dependenciesOptional based on needs:
templates/minimal-tiptap-setup.sh - shadcn component installationtemplates/image-upload-r2.tsx - R2 upload handlertemplates/tiptap-prose.css - Tailwind stylingtemplates/collaborative-setup.tsx - Y.js collaborationtemplates/common-extensions.ts - Extension bundleWhen to load these: Claude should reference templates when user asks to:
references/tiptap-docs.md - Key documentation linksreferences/common-errors.md - Error troubleshooting guidereferences/extension-catalog.md - Popular extensions listWhen Claude should load these: Troubleshooting errors, exploring extensions, understanding API
Create your own Tiptap extensions:
import { Node } from '@tiptap/core'
const CustomNode = Node.create({
name: 'customNode',
group: 'block',
content: 'inline*',
parseHTML() {
return [{ tag: 'div[data-custom]' }]
},
renderHTML({ HTMLAttributes }) {
return ['div', { 'data-custom': '', ...HTMLAttributes }, 0]
},
addCommands() {
return {
insertCustomNode: () => ({ commands }) => {
return commands.insertContent({ type: this.name })
},
}
},
})
Use cases: Custom widgets, embeds, interactive elements
Add Notion-like / commands:
import { Extension } from '@tiptap/core'
import Suggestion from '@tiptap/suggestion'
const SlashCommands = Extension.create({
name: 'slashCommands',
addOptions() {
return {
suggestion: {
char: '/',
items: ({ query }) => {
return [
{ title: 'Heading 1', command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).setHeading({ level: 1 }).run()
}},
{ title: 'Bullet List', command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).toggleBulletList().run()
}},
]
},
},
}
},
addProseMirrorPlugins() {
return [Suggestion({ editor: this.editor, ...this.options.suggestion })]
},
})
Use cases: Productivity shortcuts, quick formatting
Required:
@tiptap/react@^3.11.1 - React integration@tiptap/starter-kit@^3.11.1 - Essential extensions bundle@tiptap/pm@^3.11.1 - ProseMirror peer dependencyreact@^19.0.0 - React frameworkOptional:
@tiptap/extension-image@^3.11.1 - Image support@tiptap/extension-link@^3.11.1 - Link support (NEW in v3, included in StarterKit)@tiptap/extension-color@^3.11.1 - Text color@tiptap/extension-typography@^3.11.1 - Smart typography@tiptap/extension-collaboration@^3.11.1 - Real-time collaboration@tailwindcss/typography@^0.5.15 - Prose stylingyjs@^13.6.0 - Collaborative editing backendreact-medium-image-zoom@^5.2.0 - Image zoom functionality{
"dependencies": {
"@tiptap/react": "^3.11.1",
"@tiptap/starter-kit": "^3.11.1",
"@tiptap/pm": "^3.11.1",
"@tiptap/extension-image": "^3.11.1",
"@tiptap/extension-color": "^3.11.1",
"@tiptap/extension-text-style": "^3.11.1",
"@tiptap/extension-typography": "^3.11.1",
"@tiptap/extension-link": "^3.11.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
This skill is based on real-world implementations:
Token Savings: ~71% (14k → 4k tokens) Errors Prevented: 5/5 critical setup errors Validation: ✅ SSR compatibility, ✅ Image uploads, ✅ Tailwind v4, ✅ Performance
immediatelyRender explicitly to false"Solution: Add immediatelyRender: false to your useEditor() config
Solution: Install @tailwindcss/typography and add prose classes to editor container
Solution: Use useEditorState() hook instead of useEditor() for read-only rendering, or memoize editor configuration
Solution: Set allowBase64: false in Image extension config and use upload handler (see templates/image-upload-r2.tsx)
Solution: Switch to Vite - CRA incompatible with Tiptap v3. See cloudflare-worker-base skill for Vite setup.
Use this checklist to verify your setup:
@tiptap/react, @tiptap/starter-kit, @tiptap/pmimmediatelyRender: false in useEditor() config@tailwindcss/typography pluginprose classes to editor containerallowBase64: false in Image extensionQuestions? Issues?
references/common-errors.md for troubleshootingimmediatelyRender: false is set@tiptap/pm peer dependency is installed