ReactJS Tiptap Editor Skill
When to use
-
Building admin content editors (posts, projects, pages).
-
Implementing WYSIWYG editing with Markdown output.
-
Adding rich text capabilities to forms.
Guardrails
-
Admin-only: This editor is for authenticated admin users only.
-
Markdown output: Always export as Markdown to store in DB (content_md column).
-
Sanitize on render: When displaying content, parse Markdown → safe HTML (see content-format doc).
-
Size: This is a heavy component (~200KB); lazy-load on admin routes.
Workflow checklist
-
Install if not present: bun add reactjs-tiptap-editor
-
Import editor component in admin route/component.
-
Configure extensions (bold, italic, headings, lists, code blocks, links, images).
-
Set output="markdown" to get Markdown strings.
-
Store Markdown in DB; render with unified/remark/rehype on public pages.
Setup pattern
// domains/blog/ui/PostEditor.tsx (admin only) import { RichTextEditor } from 'reactjs-tiptap-editor' import { useState } from 'react'
export function PostEditor({ initialContent }: { initialContent?: string }) { const [content, setContent] = useState(initialContent ?? '')
return ( <RichTextEditor content={content} onChange={(newContent) => setContent(newContent)} output="markdown" // ← Critical: outputs Markdown dark={true} // Match theme extensions={[ 'bold', 'italic', 'strike', 'code', 'heading', 'bulletList', 'orderedList', 'codeBlock', 'blockquote', 'link', 'image' ]} /> ) }
Integration with domain patterns
// domains/blog/server/create-post.server.ts import { createServerFn } from '@tanstack/start' import { CreatePostInput } from '../schema'
export const createPost = createServerFn({ method: 'POST' }) .validator(CreatePostInput) .handler(async ({ data }) => { // data.content_md comes from Tiptap editor as Markdown const supabase = getSupabaseClient() const { data: post, error } = await supabase .from('posts') .insert({ ...data, content_md: data.content_md }) // ← Store Markdown .select() .single()
if (error) throw error
return post
})
Best practices
-
✅ Lazy-load: use .lazy.tsx for admin routes to avoid bloating public bundle
-
✅ Markdown output: always use output="markdown" to avoid storing HTML
-
✅ Dark mode: match dark={true} to repo's futuristic theme
-
✅ Limited extensions: only enable what users need
-
❌ Don't store raw HTML from Tiptap (security risk)
-
❌ Don't load Tiptap on public routes (bundle size)
Rendering on public pages
// src/routes/blog/$slug.tsx (public) import { parseMarkdown } from '~/lib/markdown/processor'
export const Route = createFileRoute('/blog/$slug')({ loader: async ({ params }) => { const post = await getPost(params.slug) const contentHtml = await parseMarkdown(post.content_md) // ← Safe HTML return { post, contentHtml } } })
function BlogPost() { const { contentHtml } = Route.useLoaderData() return ( <article> <div dangerouslySetInnerHTML={{ __html: contentHtml }} /> </article> ) }
References
-
Library patterns: docs/dev-1/docs/16-library-patterns.md#reactjs-tiptap-editor-rich-text
-
Content format: docs/dev-1/docs/10-content-format.md (Markdown pipeline)
-
Admin spec: docs/dev-1/docs/08-admin-dashboard-spec.md
Tooling
-
Use Tavily or Context7 for reactjs-tiptap-editor API questions.
-
Demo site: https://reactjs-tiptap-editor.vercel.app/
-
LLM context: https://reactjs-tiptap-editor.vercel.app/llms-full.txt