
Tiptap
- 80 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
tiptap is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- tiptap
- AI & Agent Building
- AI-coding skill
Tiptap by the numbers
- 80 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,257 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill tiptapAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 80 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Tiptap Rich Text Editor
Overview
Tiptap is a headless rich text editor built on ProseMirror, providing a modular extension system for React applications. It supports React 19, Tailwind v4, and SSR frameworks like Next.js. Use when building blog editors, comment systems, documentation tools, or Notion-like collaborative apps. Do NOT use with Create React App (CRA is incompatible with Tiptap v3 ESM module structure; use Vite instead).
Quick Reference
| Pattern | API / Key Point |
|---|---|
| Create editor | useEditor({ extensions: [StarterKit], immediatelyRender: false }) |
| Render editor | <EditorContent editor={editor} /> |
| Prose styling | Add className="prose dark:prose-invert max-w-none" to container |
| Configure StarterKit | StarterKit.configure({ heading: { levels: [1, 2, 3] } }) |
| Disable undo/redo | StarterKit.configure({ undoRedo: false }) (required for Y.js collab) |
| Image upload | Set allowBase64: false, use upload handler with URL replacement |
| Markdown support | import { Markdown } from '@tiptap/markdown' (official, open-source) |
| shadcn component | npx shadcn@latest add https://raw.githubusercontent.com/Aslam97/shadcn-minimal-tiptap/main/registry/block-registry.json |
| Null guard | useEditor() returns `Editor \ |
Core Dependencies
| Package | Purpose |
|---|---|
@tiptap/react | React integration (React 19 supported since v2.10.0) |
@tiptap/starter-kit | Bundled extensions: marks, nodes, and functionality |
@tiptap/pm | ProseMirror peer dependency (required, not auto-installed) |
@tailwindcss/typography | Prose styling for headings, lists, links |
StarterKit v3 Contents
| Category | Included |
|---|---|
| Marks | Bold, Italic, Strike, Code, Link (v3), Underline (v3) |
| Nodes | Document, Paragraph, Text, Heading, BulletList, OrderedList, ListItem, Blockquote, CodeBlock, HorizontalRule, HardBreak |
| Functionality | Undo/Redo, Dropcursor, Gapcursor, ListKeymap (v3), TrailingNode (v3) |
Common Additional Extensions
| Extension | Package | Use Case |
|---|---|---|
| Image | @tiptap/extension-image | Image support with resize |
| Color | @tiptap/extension-color | Text color (requires TextStyle) |
| Typography | @tiptap/extension-typography | Smart quotes, dashes, ellipsis |
| Placeholder | @tiptap/extension-placeholder | Placeholder text (requires CSS) |
| Table | @tiptap/extension-table | Table support (+ Row, Cell, Header) |
| TaskList | @tiptap/extension-task-list | Checkbox task lists |
| CodeBlockLowlight | @tiptap/extension-code-block-lowlight | Syntax-highlighted code |
| Collaboration | @tiptap/extension-collaboration | Real-time multi-user editing (Y.js) |
| Markdown | @tiptap/markdown | Bidirectional markdown (open-source) |
Common Mistakes
| Mistake | Fix |
|---|---|
Missing immediatelyRender: false | Add to useEditor() config — required for SSR/Next.js |
No prose classes on editor container | Add className="prose prose-sm dark:prose-invert max-w-none" |
| Images stored as base64 | Set allowBase64: false, use upload handler with URL replacement |
| Using EditorProvider + useEditor together | Choose one — EditorProvider wraps useEditor internally |
| Undo/Redo enabled with Collaboration | Set undoRedo: false in StarterKit when using Y.js |
| ProseMirror version conflicts | Add resolutions for prosemirror-model/view/state in package.json |
| Using Create React App | Switch to Vite — CRA incompatible with v3 ESM modules |
Not checking editor for null | useEditor() returns `Editor \ |
Using history: false for collab | Config key renamed to undoRedo in v3 |
Importing @tiptap/extension-markdown | Correct package is @tiptap/markdown |
Delegation
- Tailwind styling: see
tailwindskill - Form integration: see
tanstack-formskill
References
- Setup and Configuration
- Extensions Catalog
- Image Upload
- Patterns
- Known Issues and Errors
- Prose Styling
Extensions Catalog
StarterKit Contents
Marks: Bold, Italic, Strike, Code, Link (v3), Underline (v3)
Nodes: Document, Paragraph, Text, Heading, BulletList, OrderedList, ListItem, Blockquote, CodeBlock, HorizontalRule, HardBreak
Functionality: Undo/Redo, Dropcursor, Gapcursor, ListKeymap (v3), TrailingNode (v3)
The v3 config key for disabling undo/redo is undoRedo (renamed from history).
Additional Official Extensions
Image
import Image from '@tiptap/extension-image';
Image.configure({
inline: true,
allowBase64: false,
});Text Styling
- TextStyle —
@tiptap/extension-text-style— text style container (required by Color) - Color —
@tiptap/extension-color— text color - Highlight —
@tiptap/extension-highlight— highlighting - FontFamily —
@tiptap/extension-font-family - Subscript / Superscript
Typography
import Typography from '@tiptap/extension-typography';
// Converts: (c) -> copyright, -> -> arrow, ... -> ellipsis, "text" -> smart quotesPlaceholder
import Placeholder from '@tiptap/extension-placeholder';
Placeholder.configure({
placeholder: 'Start writing...',
emptyEditorClass: 'is-editor-empty',
});Requires CSS for the placeholder to display. See Prose Styling reference.
Tables
import Table from '@tiptap/extension-table';
import TableRow from '@tiptap/extension-table-row';
import TableCell from '@tiptap/extension-table-cell';
import TableHeader from '@tiptap/extension-table-header';
const extensions = [
Table.configure({ resizable: true }),
TableRow,
TableCell,
TableHeader,
];Task Lists
import TaskList from '@tiptap/extension-task-list';
import TaskItem from '@tiptap/extension-task-item';
const extensions = [TaskList, TaskItem.configure({ nested: true })];Code Blocks with Syntax Highlighting
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
import { common, createLowlight } from 'lowlight';
const lowlight = createLowlight(common);
CodeBlockLowlight.configure({ lowlight });Collaboration
import Collaboration from '@tiptap/extension-collaboration';
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
import * as Y from 'yjs';
const ydoc = new Y.Doc();
const extensions = [
Collaboration.configure({ document: ydoc }),
CollaborationCursor.configure({
provider,
user: { name: 'John Doe', color: '#3b82f6' },
}),
];Markdown
import { Markdown } from '@tiptap/markdown';
const editor = useEditor({
extensions: [StarterKit, Markdown],
content: '# Hello',
contentType: 'markdown',
immediatelyRender: false,
});
const md = editor.getMarkdown();
editor.commands.setContent('## New heading', { contentType: 'markdown' });Package: @tiptap/markdown (not @tiptap/extension-markdown).
Utilities
- CharacterCount —
@tiptap/extension-character-count - Focus —
@tiptap/extension-focus - TextAlign —
@tiptap/extension-text-align
Pro Extensions (Paid)
Content AI, AI Image, Comments, FileHandler, Mathematics, TableOfContents, UniqueID.
Community Extensions
| Extension | Purpose |
|---|---|
| tiptap-extension-global-drag-handle | Notion-like block drag handles |
| @tiptap-pro/extension-emoji | Emoji picker |
| tiptap-youtube | YouTube embeds |
| @joeattardi/tiptap-indent | Text indentation |
| tiptap-text-direction | RTL/LTR support |
| tiptap-slash-command | Slash command menu |
| tiptap-extension-details-summary | Collapsible sections |
Custom Extension Development
Node Template
import { Node } from '@tiptap/core';
export 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 });
},
};
},
});Mark Template
import { Mark } from '@tiptap/core';
export const CustomMark = Mark.create({
name: 'customMark',
parseHTML() {
return [{ tag: 'span[data-custom]' }];
},
renderHTML({ HTMLAttributes }) {
return ['span', { 'data-custom': '', ...HTMLAttributes }, 0];
},
addCommands() {
return {
toggleCustomMark:
() =>
({ commands }) => {
return commands.toggleMark(this.name);
},
};
},
});Extension Template
import { Extension } from '@tiptap/core';
export const CustomExtension = Extension.create({
name: 'customExtension',
addOptions() {
return {};
},
addCommands() {
return {};
},
addKeyboardShortcuts() {
return {
'Mod-Shift-x': () => this.editor.commands.toggleCustomMark(),
};
},
});Installation Quick Reference
# Core
npm install @tiptap/react @tiptap/starter-kit @tiptap/pm
# Media
npm install @tiptap/extension-image
# Text Styling
npm install @tiptap/extension-text-style @tiptap/extension-color @tiptap/extension-highlight
# Content
npm install @tiptap/extension-typography @tiptap/extension-placeholder
# Tables
npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-header
# Task Lists
npm install @tiptap/extension-task-list @tiptap/extension-task-item
# Code Blocks with Syntax Highlighting
npm install @tiptap/extension-code-block-lowlight lowlight
# Collaboration
npm install @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor yjs
# Markdown
npm install @tiptap/markdown
# Utilities
npm install @tiptap/extension-character-count @tiptap/extension-focus @tiptap/extension-text-alignImage Upload
Pattern: Base64 Preview → Upload → URL Replace
1. Create base64 preview for immediate display 2. Insert preview into editor 3. Upload to R2/S3 in background 4. Replace base64 with permanent URL
import { Editor } from '@tiptap/core';
async function uploadImageToR2(editor: Editor, file: File): Promise<string> {
const reader = new FileReader();
const base64 = await new Promise<string>((resolve) => {
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(file);
});
editor.chain().focus().setImage({ src: base64 }).run();
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});
const { url } = await response.json();
editor.chain().focus().updateAttributes('image', { src: url }).run();
return url;
}Image Extension Configuration
Always disable base64 to prevent bloat:
import Image from '@tiptap/extension-image';
Image.configure({
inline: true,
allowBase64: false,
resize: {
enabled: true,
directions: ['top-right', 'bottom-right', 'bottom-left', 'top-left'],
minWidth: 100,
minHeight: 100,
alwaysPreserveAspectRatio: true,
},
});Why This Pattern
- Immediate user feedback (base64 preview appears instantly)
- No database bloat from base64 strings
- Works with any object storage (Cloudflare R2, AWS S3, etc.)
- Graceful error handling — if upload fails, preview is still visible
Known Issues and Errors
Issue #1: SSR Hydration Mismatch
Error: "SSR has been detected, please set immediatelyRender explicitly to false"
Tiptap defaults to immediatelyRender: true, causing server/client HTML mismatch.
Fix: Set immediatelyRender: false in useEditor().
Issue #2: Editor Re-renders on Every Keystroke
Symptom: Laggy typing, poor performance in large documents.
useEditor() re-renders the component on every change.
Fix: Use useEditorState() for read-only rendering, or memoize extensions:
const extensions = useMemo(() => [StarterKit, Image, Link], []);
const editor = useEditor({ extensions, immediatelyRender: false });Lazy load extensions when possible:
const extensions = [
StarterKit,
...(needsTables ? [Table, TableRow, TableCell] : []),
];Issue #3: Tailwind Typography Not Working
Symptom: Headings/lists render unstyled.
Fix: Install @tailwindcss/typography and add prose classes:
<EditorContent
editor={editor}
className="prose prose-sm dark:prose-invert max-w-none"
/>Issue #4: Image Upload Base64 Bloat
Symptom: JSON payloads become megabytes.
Fix: Set allowBase64: false and implement upload handler. See Image Upload.
Issue #5: Build Fails in Create React App
Error: "jsx-runtime" module resolution errors.
CRA is incompatible with Tiptap v3 ESM module structure.
Fix: Switch to Vite or another modern bundler.
Issue #6: ProseMirror Multiple Versions Conflict
Error: "Looks like multiple versions of prosemirror-model were loaded"
Installing additional Tiptap extensions can pull different ProseMirror versions.
Fix: Add resolutions to package.json:
{
"resolutions": {
"prosemirror-model": "~1.21.0",
"prosemirror-view": "~1.33.0",
"prosemirror-state": "~1.4.3"
}
}Or clean reinstall:
rm -rf node_modules package-lock.json && npm installIssue #7: EditorProvider vs useEditor Confusion
Using both together causes SSR errors. EditorProvider is a wrapper around useEditor for React Context — they should not be used simultaneously.
Fix: Choose one:
// Option 1: EditorProvider only
<EditorProvider immediatelyRender={false} extensions={[StarterKit]}>
<EditorContent />
</EditorProvider>;
// Option 2: useEditor only
const editor = useEditor({
extensions: [StarterKit],
immediatelyRender: false,
});Issue #8: TypeScript Null Errors
Error: "Type 'Editor | null' is not assignable to type 'Editor'"
useEditor() returns Editor | null. Editor is null during initial render.
Fix: Guard before use:
const editor = useEditor({
extensions: [StarterKit],
immediatelyRender: false,
});
if (!editor) return null;
// Event handlers
<button
disabled={!editor}
onClick={() => editor?.chain().focus().toggleBold().run()}
>
Bold
</button>;Issue #9: Extensions Not Working
Symptom: Extension installed but commands don't work, no errors.
Fix: Ensure extension is imported AND added to the extensions array. Check with:
console.log(editor.extensionManager.extensions.map((e) => e.name));Issue #10: Content Not Updating
Symptom: Editor doesn't reflect prop changes.
Fix: Sync with useEffect, never call setContent during render:
useEffect(() => {
if (editor && content !== editor.getHTML()) {
editor.commands.setContent(content);
}
}, [content, editor]);Issue #11: Placeholder Not Showing
Fix: Install extension AND add CSS:
.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: var(--muted-foreground);
float: left;
height: 0;
pointer-events: none;
}Issue #12: Collaborative Editing Conflicts
Symptom: Content overwrites, cursor positions wrong, undo/redo breaks.
Fix: Disable undo/redo when using Y.js. In v3, use undoRedo: false (renamed from history):
StarterKit.configure({ undoRedo: false });Debugging Tips
const editor = useEditor({
extensions: [StarterKit],
onBeforeCreate: ({ editor }) => console.log('Creating:', editor),
onCreate: ({ editor }) => console.log('Created:', editor),
onUpdate: ({ editor }) => console.log('Updated:', editor.getJSON()),
});
// Inspect state
console.log(editor.getJSON());
console.log(editor.getHTML());
console.log(editor.state);
console.log(editor.extensionManager.extensions.map((e) => e.name));
console.log(editor.isEditable);
console.log(editor.state.selection);
console.log(editor.getAttributes('heading'));Patterns
Collaborative Editing with Y.js
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Collaboration from '@tiptap/extension-collaboration';
import * as Y from 'yjs';
const ydoc = new Y.Doc();
const editor = useEditor({
extensions: [
StarterKit.configure({
undoRedo: false,
}),
Collaboration.configure({
document: ydoc,
}),
],
immediatelyRender: false,
});Disable undo/redo when using Collaboration — local undo/redo conflicts with Y.js CRDT. In v3, the config key is undoRedo (renamed from history).
Markdown Support
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { Markdown } from '@tiptap/markdown';
const editor = useEditor({
extensions: [StarterKit, Markdown],
content: '# Hello World\n\nThis is **Markdown**!',
contentType: 'markdown',
immediatelyRender: false,
});
const markdownOutput = editor.getMarkdown();
editor.commands.setContent('## New heading', { contentType: 'markdown' });
editor.commands.insertContent('**Bold** text', { contentType: 'markdown' });Install: npm install @tiptap/markdown
Always specify contentType: 'markdown' when setting markdown content — without it, content is parsed as HTML.
Form Integration with react-hook-form
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>
);
}Slash 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 })];
},
});Content Syncing
Sync external content changes with useEffect:
import { useEffect } from 'react';
function Editor({ content }: { content: string }) {
const editor = useEditor({
extensions: [StarterKit],
immediatelyRender: false,
});
useEffect(() => {
if (editor && content !== editor.getHTML()) {
editor.commands.setContent(content);
}
}, [content, editor]);
return <EditorContent editor={editor} />;
}Do not call setContent during render — always use useEffect.
Prose Styling
Tailwind Prose Classes
Apply to the editor container:
<EditorContent
editor={editor}
className="prose prose-sm sm:prose-base lg:prose-lg dark:prose-invert max-w-none"
/>proseprovides consistent formatting for headings, lists, linksdark:prose-inverthandles dark mode automaticallymax-w-noneremoves the default max-width
Custom CSS Overrides
.tiptap {
@apply prose prose-sm sm:prose-base lg:prose-lg dark:prose-invert max-w-none;
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;
}
}Uses semantic Tailwind v4 colors (bg-muted, border-primary).
Placeholder Styling
.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: var(--muted-foreground);
float: left;
height: 0;
pointer-events: none;
}Requires the Placeholder extension with emptyEditorClass: 'is-editor-empty'.
Setup and Configuration
Installation
npm install @tiptap/react @tiptap/starter-kit @tiptap/pm@tiptap/pmis a required peer dependency (ProseMirror engine)- StarterKit bundles marks, nodes, and functionality extensions
- Additional extensions (Image, Color, Typography) installed separately as needed
SSR-Safe Editor
'use client';
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,
editorProps: {
attributes: {
class: 'prose prose-sm focus:outline-none min-h-[200px] p-4',
},
},
});
if (!editor) return null;
return <EditorContent editor={editor} />;
}immediatelyRender: false is required for Next.js/SSR apps. Without it: "SSR has been detected, please set immediatelyRender explicitly to false". This is the most common Tiptap error.
Tailwind Typography
npm install @tailwindcss/typographyimport typography from '@tailwindcss/typography';
export default {
plugins: [typography],
};Without this, formatted content (headings, lists, links) looks unstyled.
Extension Configuration
import StarterKit from '@tiptap/starter-kit';
import Image from '@tiptap/extension-image';
import Typography from '@tiptap/extension-typography';
const editor = useEditor({
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
bulletList: { keepMarks: true },
}),
Image.configure({
inline: true,
allowBase64: false,
}),
Typography,
],
immediatelyRender: false,
});Extension order matters — dependencies must load first. Set allowBase64: false to prevent huge JSON payloads.
Link and Underline are included in StarterKit v3 by default. Do not install them separately.
React Version Compatibility
| Scope | React 19 | React 18 |
|---|---|---|
| Core Tiptap | Supported (v2.10.0+) | Supported |
| UI Components | Works | Recommended by official docs |
| Pro Extensions | May require React 18 | Full support |
Pro drag-handle extension depends on archived tippyjs-react without React 19 support.
Bundler Compatibility
Tiptap v3 ships as ESM. Create React App (CRA) is incompatible with the v3 module structure. Use Vite or another modern bundler.
Package Version Resolutions
If you encounter ProseMirror multiple version conflicts:
{
"resolutions": {
"prosemirror-model": "~1.21.0",
"prosemirror-view": "~1.33.0",
"prosemirror-state": "~1.4.3"
}
}shadcn Minimal Tiptap
Pre-built component with toolbar, dark mode, and image upload:
npx shadcn@latest add https://raw.githubusercontent.com/Aslam97/shadcn-minimal-tiptap/main/registry/block-registry.jsonRequires wrapping the app with TooltipProvider from @/components/ui/tooltip.