├── .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 |
458 | )}
459 |
460 | )
461 | }
462 |
463 | export default App
464 |
--------------------------------------------------------------------------------