├── .env.example ├── src ├── app │ ├── favicon.ico │ ├── page.tsx │ ├── layout.tsx │ └── globals.css └── components │ └── Editor │ ├── EditorClient.tsx │ └── Editor.tsx ├── postcss.config.js ├── tailwind.config.ts ├── next.config.mjs ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_GIPHY_API_KEY= 2 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor-demo/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import EditorClient from "@/components/Editor/EditorClient"; 2 | 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | export default { 4 | content: ['src/**/*.{ts,tsx}'], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } satisfies Config; 10 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | eslint: { 4 | ignoreDuringBuilds: true, 5 | }, 6 | typescript: { 7 | ignoreBuildErrors: true, 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /src/components/Editor/EditorClient.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import dynamic from 'next/dynamic'; 4 | import React from 'react'; 5 | 6 | const Editor = dynamic(() => import('@/components/Editor/Editor'), { 7 | ssr: false, 8 | loading: () =>

Loading...

, 9 | }); 10 | 11 | const EditorClient = () => { 12 | return ( 13 | <> 14 | 15 | 16 | ); 17 | } 18 | 19 | 20 | 21 | export default EditorClient; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env 39 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | * { 7 | box-sizing: border-box; 8 | padding: 0; 9 | margin: 0; 10 | } 11 | 12 | html, 13 | body { 14 | max-width: 100vw; 15 | overflow-x: hidden; 16 | } 17 | 18 | 19 | .header button { 20 | padding: 8px 16px; 21 | border: none; 22 | border-radius: 5px; 23 | background-color: #d1d2d2; 24 | color: rgb(0, 0, 0); 25 | cursor: pointer; 26 | } 27 | 28 | .header select { 29 | padding: 8px 16px; 30 | border: none; 31 | border-radius: 5px; 32 | background-color: #d1d2d2; 33 | color: rgb(0, 0, 0); 34 | cursor: pointer; 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | }, 29 | "target": "ES2017" 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-tiptap", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 3003", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@tiptap/extension-document": "^3.13.0", 13 | "@tiptap/extension-hard-break": "^3.13.0", 14 | "@tiptap/extension-list": "^3.13.0", 15 | "@tiptap/extension-paragraph": "^3.13.0", 16 | "@tiptap/extension-text": "^3.13.0", 17 | "@tiptap/extension-text-style": "^3.13.0", 18 | "@tiptap/extensions": "^3.13.0", 19 | "@tiptap/react": "^3.13.0", 20 | "next": "15.3.8", 21 | "react": "^19.1.0", 22 | "react-dom": "^19.1.0", 23 | "reactjs-tiptap-editor": "^1.0.7" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20", 27 | "@types/react": "^19", 28 | "@types/react-dom": "^19", 29 | "autoprefixer": "10.4.20", 30 | "eslint": "^9", 31 | "eslint-config-next": "15.2.1", 32 | "postcss": "8.4.47", 33 | "tailwindcss": "3.4.13", 34 | "typescript": "^5" 35 | }, 36 | "packageManager": "pnpm@8.15.9" 37 | } 38 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | 3 | import { RichTextProvider } from 'reactjs-tiptap-editor' 4 | 5 | import { localeActions } from 'reactjs-tiptap-editor/locale-bundle' 6 | 7 | import { themeActions } from 'reactjs-tiptap-editor/theme' 8 | 9 | // Base Kit 10 | import { Document } from '@tiptap/extension-document' 11 | import { HardBreak } from '@tiptap/extension-hard-break' 12 | import { ListItem } from '@tiptap/extension-list' 13 | import { Paragraph } from '@tiptap/extension-paragraph' 14 | import { Text } from '@tiptap/extension-text' 15 | import { TextStyle } from '@tiptap/extension-text-style' 16 | import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions' 17 | 18 | // build extensions 19 | import { Attachment, RichTextAttachment } from 'reactjs-tiptap-editor/attachment' 20 | import { Blockquote, RichTextBlockquote } from 'reactjs-tiptap-editor/blockquote' 21 | import { Bold, RichTextBold } from 'reactjs-tiptap-editor/bold' 22 | import { BulletList, RichTextBulletList } from 'reactjs-tiptap-editor/bulletlist' 23 | import { Clear, RichTextClear } from 'reactjs-tiptap-editor/clear' 24 | import { Code, RichTextCode } from 'reactjs-tiptap-editor/code' 25 | import { CodeBlock, RichTextCodeBlock } from 'reactjs-tiptap-editor/codeblock' 26 | import { CodeView, RichTextCodeView } from 'reactjs-tiptap-editor/codeview' 27 | import { Color, RichTextColor } from 'reactjs-tiptap-editor/color' 28 | import { Column, ColumnNode, MultipleColumnNode, RichTextColumn } from 'reactjs-tiptap-editor/column' 29 | import { Drawer, RichTextDrawer } from 'reactjs-tiptap-editor/drawer' 30 | import { Emoji, RichTextEmoji } from 'reactjs-tiptap-editor/emoji' 31 | import { Excalidraw, RichTextExcalidraw } from 'reactjs-tiptap-editor/excalidraw' 32 | import { ExportPdf, RichTextExportPdf } from 'reactjs-tiptap-editor/exportpdf' 33 | import { ExportWord, RichTextExportWord } from 'reactjs-tiptap-editor/exportword' 34 | import { FontFamily, RichTextFontFamily } from 'reactjs-tiptap-editor/fontfamily' 35 | import { FontSize, RichTextFontSize } from 'reactjs-tiptap-editor/fontsize' 36 | import { Heading, RichTextHeading } from 'reactjs-tiptap-editor/heading' 37 | import { Highlight, RichTextHighlight } from 'reactjs-tiptap-editor/highlight' 38 | import { History, RichTextRedo, RichTextUndo } from 'reactjs-tiptap-editor/history' 39 | import { HorizontalRule, RichTextHorizontalRule } from 'reactjs-tiptap-editor/horizontalrule' 40 | import { Iframe, RichTextIframe } from 'reactjs-tiptap-editor/iframe' 41 | import { Image, RichTextImage } from 'reactjs-tiptap-editor/image' 42 | import { ImageGif, RichTextImageGif } from 'reactjs-tiptap-editor/imagegif' 43 | import { ImportWord, RichTextImportWord } from 'reactjs-tiptap-editor/importword' 44 | import { Indent, RichTextIndent } from 'reactjs-tiptap-editor/indent' 45 | import { Italic, RichTextItalic } from 'reactjs-tiptap-editor/italic' 46 | import { Katex, RichTextKatex } from 'reactjs-tiptap-editor/katex' 47 | import { LineHeight, RichTextLineHeight } from 'reactjs-tiptap-editor/lineheight' 48 | import { Link, RichTextLink } from 'reactjs-tiptap-editor/link' 49 | import { Mention } from 'reactjs-tiptap-editor/mention' 50 | import { Mermaid, RichTextMermaid } from 'reactjs-tiptap-editor/mermaid' 51 | import { MoreMark, RichTextMoreMark } from 'reactjs-tiptap-editor/moremark' 52 | import { OrderedList, RichTextOrderedList } from 'reactjs-tiptap-editor/orderedlist' 53 | import { RichTextSearchAndReplace, SearchAndReplace } from 'reactjs-tiptap-editor/searchandreplace' 54 | import { RichTextStrike, Strike } from 'reactjs-tiptap-editor/strike' 55 | import { RichTextTable, Table } from 'reactjs-tiptap-editor/table' 56 | import { RichTextTaskList, TaskList } from 'reactjs-tiptap-editor/tasklist' 57 | import { RichTextAlign, TextAlign } from 'reactjs-tiptap-editor/textalign' 58 | import { RichTextTextDirection, TextDirection } from 'reactjs-tiptap-editor/textdirection' 59 | import { RichTextUnderline, TextUnderline } from 'reactjs-tiptap-editor/textunderline' 60 | import { RichTextTwitter, Twitter } from 'reactjs-tiptap-editor/twitter' 61 | import { RichTextVideo, Video } from 'reactjs-tiptap-editor/video' 62 | 63 | // Slash Command 64 | import { SlashCommand, SlashCommandList } from 'reactjs-tiptap-editor/slashcommand' 65 | 66 | 67 | // Bubble 68 | import { 69 | RichTextBubbleColumns, 70 | RichTextBubbleDrawer, 71 | RichTextBubbleExcalidraw, 72 | RichTextBubbleIframe, 73 | RichTextBubbleImage, 74 | RichTextBubbleImageGif, 75 | RichTextBubbleKatex, 76 | RichTextBubbleLink, 77 | RichTextBubbleMermaid, 78 | RichTextBubbleTable, 79 | RichTextBubbleText, 80 | RichTextBubbleTwitter, 81 | RichTextBubbleVideo 82 | } from 'reactjs-tiptap-editor/bubble' 83 | 84 | import "@excalidraw/excalidraw/index.css" 85 | import 'easydrawer/styles.css' 86 | import 'katex/dist/katex.min.css' 87 | import 'prism-code-editor-lightweight/layout.css' 88 | import "prism-code-editor-lightweight/themes/github-dark.css" 89 | import 'reactjs-tiptap-editor/style.css' 90 | 91 | import { EditorContent, useEditor } from '@tiptap/react' 92 | 93 | function convertBase64ToBlob(base64: string) { 94 | const arr = base64.split(',') 95 | const mime = arr[0].match(/:(.*?);/)![1] 96 | const bstr = atob(arr[1]) 97 | let n = bstr.length 98 | const u8arr = new Uint8Array(n) 99 | while (n--) { 100 | u8arr[n] = bstr.charCodeAt(n) 101 | } 102 | return new Blob([u8arr], { type: mime }) 103 | } 104 | 105 | // custom document to support columns 106 | const DocumentColumn = /* @__PURE__ */ Document.extend({ 107 | content: '(block|columns)+', 108 | // echo editor is a block editor 109 | }); 110 | 111 | const BaseKit = [ 112 | DocumentColumn, 113 | Text, 114 | Dropcursor, 115 | Gapcursor, 116 | HardBreak, 117 | Paragraph, 118 | TrailingNode, 119 | ListItem, 120 | TextStyle, 121 | Placeholder.configure({ 122 | placeholder: 'Press \'/\' for commands', 123 | }) 124 | ] 125 | 126 | const extensions = [ 127 | ...BaseKit, 128 | 129 | History, 130 | SearchAndReplace, 131 | Clear, 132 | FontFamily, 133 | Heading, 134 | FontSize, 135 | Bold, 136 | Italic, 137 | TextUnderline, 138 | Strike, 139 | MoreMark, 140 | Emoji, 141 | Color, 142 | Highlight, 143 | BulletList, 144 | OrderedList, 145 | TextAlign, 146 | Indent, 147 | LineHeight, 148 | TaskList, 149 | Link, 150 | Image.configure({ 151 | upload: (files: File) => { 152 | return new Promise((resolve) => { 153 | setTimeout(() => { 154 | resolve(URL.createObjectURL(files)) 155 | }, 300) 156 | }) 157 | }, 158 | }), 159 | Video.configure({ 160 | upload: (files: File) => { 161 | return new Promise((resolve) => { 162 | setTimeout(() => { 163 | resolve(URL.createObjectURL(files)) 164 | }, 300) 165 | }) 166 | }, 167 | }), 168 | ImageGif.configure({ 169 | provider: 'giphy', 170 | API_KEY: process.env.NEXT_PUBLIC_GIPHY_API_KEY as string 171 | }), 172 | Blockquote, 173 | HorizontalRule, 174 | Code, 175 | CodeBlock, 176 | 177 | Column, 178 | ColumnNode, 179 | MultipleColumnNode, 180 | Table, 181 | Iframe, 182 | ExportPdf, 183 | ImportWord, 184 | ExportWord, 185 | TextDirection, 186 | Attachment.configure({ 187 | upload: (file: any) => { 188 | // fake upload return base 64 189 | const reader = new FileReader() 190 | reader.readAsDataURL(file) 191 | 192 | return new Promise((resolve) => { 193 | setTimeout(() => { 194 | const blob = convertBase64ToBlob(reader.result as string) 195 | resolve(URL.createObjectURL(blob)) 196 | }, 300) 197 | }) 198 | }, 199 | }), 200 | Katex, 201 | Excalidraw, 202 | Mermaid.configure({ 203 | upload: (file: any) => { 204 | // fake upload return base 64 205 | const reader = new FileReader() 206 | reader.readAsDataURL(file) 207 | 208 | return new Promise((resolve) => { 209 | setTimeout(() => { 210 | const blob = convertBase64ToBlob(reader.result as string) 211 | resolve(URL.createObjectURL(blob)) 212 | }, 300) 213 | }) 214 | }, 215 | }), 216 | Drawer.configure({ 217 | upload: (file: any) => { 218 | // fake upload return base 64 219 | const reader = new FileReader() 220 | reader.readAsDataURL(file) 221 | 222 | return new Promise((resolve) => { 223 | setTimeout(() => { 224 | const blob = convertBase64ToBlob(reader.result as string) 225 | resolve(URL.createObjectURL(blob)) 226 | }, 300) 227 | }) 228 | }, 229 | }), 230 | Twitter, 231 | Mention, 232 | SlashCommand, 233 | CodeView, 234 | ] 235 | 236 | const DEFAULT = `

Rich Text Editor

A modern WYSIWYG rich text editor based on tiptap and shadcn ui for Reactjs


Demo

👉Demo

Features

Installation

pnpm install reactjs-tiptap-editor

` 237 | 238 | function debounce(func: any, wait: number) { 239 | let timeout: NodeJS.Timeout 240 | return function (...args: any[]) { 241 | clearTimeout(timeout) 242 | // @ts-ignore 243 | timeout = setTimeout(() => func.apply(this, args), wait) 244 | } 245 | } 246 | 247 | 248 | const Header = ({ editor }) => { 249 | const [theme, setTheme] = useState('light') 250 | const [editorEditable, setEditorEditable] = useState(true); 251 | const [color, setColor] = useState('default'); 252 | const [lang, setlang] = useState('en'); 253 | 254 | useEffect(() => { 255 | if (editor) { 256 | editor.on('update', () => { 257 | setEditorEditable(editor.isEditable); 258 | }) 259 | } 260 | 261 | return () => { 262 | if (editor) { 263 | editor.off('update', () => { 264 | setEditorEditable(editor.isEditable); 265 | }) 266 | } 267 | } 268 | }, [editor]); 269 | 270 | return <> 271 |
272 |
273 | 284 | 285 | 288 | 289 | 296 | 297 | 304 |
305 | 306 |
307 | 313 | 314 | 327 |
328 |
329 | 330 | } 331 | 332 | const RichTextToolbar = () => { 333 | return
334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 |
378 | } 379 | 380 | function App() { 381 | const [content, setContent] = useState(DEFAULT) 382 | 383 | const onValueChange = useCallback( 384 | debounce((value: any) => { 385 | setContent(value) 386 | }, 300), 387 | [], 388 | ) 389 | 390 | const editor = useEditor({ 391 | // shouldRerenderOnTransaction: false, 392 | textDirection: 'auto', // global text direction 393 | content, 394 | extensions, 395 | // content, 396 | immediatelyRender: false, // error duplicate plugin key 397 | onUpdate: ({ editor }) => { 398 | const html = editor.getHTML() 399 | onValueChange(html) 400 | }, 401 | }); 402 | 403 | useEffect(() => { 404 | window['editor'] = editor; 405 | }, [editor]); 406 | 407 | return ( 408 |
415 |
416 | 417 | 418 |
419 |
420 | 421 | 422 | 425 | 426 | {/* Bubble */} 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | {/* Command List */} 444 | 445 |
446 |
447 |
448 | 449 | {typeof content === 'string' && ( 450 |