├── .env.example ├── .gitignore ├── README.md ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx └── components │ └── Editor │ ├── Editor.tsx │ └── EditorClient.tsx └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_GIPHY_API_KEY= 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "next": "15.3.0", 13 | "react": "^19.1.0", 14 | "react-dom": "^19.1.0", 15 | "reactjs-tiptap-editor": "^0.3.1" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20", 19 | "@types/react": "^19", 20 | "@types/react-dom": "^19", 21 | "eslint": "^9", 22 | "eslint-config-next": "15.2.1", 23 | "typescript": "^5" 24 | }, 25 | "packageManager": "pnpm@8.15.9" 26 | } 27 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hunghg255/reactjs-tiptap-editor-demo/251b4bc1616e100398fe76b56fea4b2fce77f5af/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", 5 | "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", 6 | "Fira Mono", "Droid Sans Mono", "Courier New", monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | 89 | .buttonWrap { 90 | display: flex; 91 | justify-content: center; 92 | align-items: center; 93 | margin-top: 20px; 94 | } 95 | 96 | .buttonWrap button { 97 | padding: 10px 20px; 98 | border: none; 99 | border-radius: 5px; 100 | background-color: #d1d2d2; 101 | color: rgb(0, 0, 0); 102 | cursor: pointer; 103 | } 104 | 105 | .dark body { 106 | background-color: #0a0b0b; 107 | } 108 | 109 | .textarea { 110 | border: 1px solid #ccc; 111 | background: #f9f9f9; 112 | color: #333; 113 | } 114 | 115 | .dark .textarea { 116 | border: 1px solid #666; 117 | background: #0a0b0b; 118 | color: #fff; 119 | } 120 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/Editor/Editor.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | /* eslint-disable unicorn/no-null */ 4 | /* eslint-disable quotes */ 5 | import { useCallback, useState } from 'react' 6 | 7 | import RichTextEditor, { BaseKit } from 'reactjs-tiptap-editor' 8 | 9 | import { locale } from 'reactjs-tiptap-editor/locale-bundle' 10 | import { 11 | BubbleMenuTwitter, 12 | BubbleMenuKatex, 13 | BubbleMenuExcalidraw, 14 | BubbleMenuMermaid, 15 | BubbleMenuDrawer 16 | } from 'reactjs-tiptap-editor/bubble-extra'; 17 | 18 | import { Attachment } from 'reactjs-tiptap-editor/attachment'; 19 | import { Blockquote } from 'reactjs-tiptap-editor/blockquote'; 20 | import { Bold } from 'reactjs-tiptap-editor/bold'; 21 | import { BulletList } from 'reactjs-tiptap-editor/bulletlist'; 22 | import { Clear } from 'reactjs-tiptap-editor/clear'; 23 | import { Code } from 'reactjs-tiptap-editor/code'; 24 | import { CodeBlock } from 'reactjs-tiptap-editor/codeblock'; 25 | import { Color } from 'reactjs-tiptap-editor/color'; 26 | import { ColumnActionButton } from 'reactjs-tiptap-editor/multicolumn'; 27 | import { Emoji } from 'reactjs-tiptap-editor/emoji'; 28 | import { ExportPdf } from 'reactjs-tiptap-editor/exportpdf'; 29 | import { ExportWord } from 'reactjs-tiptap-editor/exportword'; 30 | import { FontFamily } from 'reactjs-tiptap-editor/fontfamily'; 31 | import { FontSize } from 'reactjs-tiptap-editor/fontsize'; 32 | import { FormatPainter } from 'reactjs-tiptap-editor/formatpainter'; 33 | import { Heading } from 'reactjs-tiptap-editor/heading'; 34 | import { Highlight } from 'reactjs-tiptap-editor/highlight'; 35 | import { History } from 'reactjs-tiptap-editor/history'; 36 | import { HorizontalRule } from 'reactjs-tiptap-editor/horizontalrule'; 37 | import { Iframe } from 'reactjs-tiptap-editor/iframe'; 38 | import { Image } from 'reactjs-tiptap-editor/image'; 39 | import { ImageGif } from 'reactjs-tiptap-editor/imagegif'; 40 | import { ImportWord } from 'reactjs-tiptap-editor/importword'; 41 | import { Indent } from 'reactjs-tiptap-editor/indent'; 42 | import { Italic } from 'reactjs-tiptap-editor/italic'; 43 | import { LineHeight } from 'reactjs-tiptap-editor/lineheight'; 44 | import { Link } from 'reactjs-tiptap-editor/link'; 45 | import { Mention } from 'reactjs-tiptap-editor/mention'; 46 | import { MoreMark } from 'reactjs-tiptap-editor/moremark'; 47 | import { OrderedList } from 'reactjs-tiptap-editor/orderedlist'; 48 | import { SearchAndReplace } from 'reactjs-tiptap-editor/searchandreplace'; 49 | import { SlashCommand } from 'reactjs-tiptap-editor/slashcommand'; 50 | import { Strike } from 'reactjs-tiptap-editor/strike'; 51 | import { Table } from 'reactjs-tiptap-editor/table'; 52 | import { TableOfContents } from 'reactjs-tiptap-editor/tableofcontent'; 53 | import { TaskList } from 'reactjs-tiptap-editor/tasklist'; 54 | import { TextAlign } from 'reactjs-tiptap-editor/textalign'; 55 | import { TextUnderline } from 'reactjs-tiptap-editor/textunderline'; 56 | import { Video } from 'reactjs-tiptap-editor/video'; 57 | import { TextDirection } from 'reactjs-tiptap-editor/textdirection'; 58 | import { Katex } from 'reactjs-tiptap-editor/katex'; 59 | import { Drawer } from 'reactjs-tiptap-editor/drawer'; 60 | import { Excalidraw } from 'reactjs-tiptap-editor/excalidraw'; 61 | import { Twitter } from 'reactjs-tiptap-editor/twitter'; 62 | import { Mermaid } from 'reactjs-tiptap-editor/mermaid'; 63 | 64 | import 'reactjs-tiptap-editor/style.css' 65 | import 'prism-code-editor-lightweight/layout.css'; 66 | import "prism-code-editor-lightweight/themes/github-dark.css" 67 | 68 | import 'katex/dist/katex.min.css' 69 | import 'easydrawer/styles.css' 70 | import 'react-image-crop/dist/ReactCrop.css'; 71 | 72 | function convertBase64ToBlob(base64: string) { 73 | const arr = base64.split(',') 74 | const mime = arr[0].match(/:(.*?);/)![1] 75 | const bstr = atob(arr[1]) 76 | let n = bstr.length 77 | const u8arr = new Uint8Array(n) 78 | while (n--) { 79 | u8arr[n] = bstr.charCodeAt(n) 80 | } 81 | return new Blob([u8arr], { type: mime }) 82 | } 83 | 84 | const extensions = [ 85 | BaseKit.configure({ 86 | placeholder: { 87 | showOnlyCurrent: true, 88 | }, 89 | characterCount: { 90 | limit: 50_000, 91 | }, 92 | }), 93 | History, 94 | SearchAndReplace, 95 | TableOfContents, 96 | FormatPainter.configure({ spacer: true }), 97 | Clear, 98 | FontFamily, 99 | Heading.configure({ spacer: true }), 100 | FontSize, 101 | Bold, 102 | Italic, 103 | TextUnderline, 104 | Strike, 105 | MoreMark, 106 | Emoji, 107 | Color.configure({ spacer: true }), 108 | Highlight, 109 | BulletList, 110 | OrderedList, 111 | TextAlign.configure({ types: ['heading', 'paragraph'], spacer: true }), 112 | Indent, 113 | LineHeight, 114 | TaskList.configure({ 115 | spacer: true, 116 | taskItem: { 117 | nested: true, 118 | }, 119 | }), 120 | Link, 121 | Image.configure({ 122 | upload: (files: File) => { 123 | return new Promise((resolve) => { 124 | setTimeout(() => { 125 | resolve(URL.createObjectURL(files)) 126 | }, 500) 127 | }) 128 | }, 129 | }), 130 | Video.configure({ 131 | upload: (files: File) => { 132 | return new Promise((resolve) => { 133 | setTimeout(() => { 134 | resolve(URL.createObjectURL(files)) 135 | }, 500) 136 | }) 137 | }, 138 | }), 139 | ImageGif.configure({ 140 | GIPHY_API_KEY: process.env.NEXT_PUBLIC_GIPHY_API_KEY as string, 141 | }), 142 | Blockquote, 143 | SlashCommand, 144 | HorizontalRule, 145 | Code.configure({ 146 | toolbar: false, 147 | }), 148 | CodeBlock, 149 | ColumnActionButton, 150 | Table, 151 | Iframe, 152 | ExportPdf.configure({ spacer: true }), 153 | ImportWord.configure({ 154 | upload: (files: File[]) => { 155 | const f = files.map(file => ({ 156 | src: URL.createObjectURL(file), 157 | alt: file.name, 158 | })) 159 | return Promise.resolve(f) 160 | }, 161 | }), 162 | ExportWord, 163 | TextDirection, 164 | Mention, 165 | Attachment.configure({ 166 | upload: (file: any) => { 167 | // fake upload return base 64 168 | const reader = new FileReader() 169 | reader.readAsDataURL(file) 170 | 171 | return new Promise((resolve) => { 172 | setTimeout(() => { 173 | const blob = convertBase64ToBlob(reader.result as string) 174 | resolve(URL.createObjectURL(blob)) 175 | }, 300) 176 | }) 177 | }, 178 | }), 179 | 180 | Katex, 181 | Excalidraw, 182 | Mermaid.configure({ 183 | upload: (file: any) => { 184 | // fake upload return base 64 185 | const reader = new FileReader() 186 | reader.readAsDataURL(file) 187 | 188 | return new Promise((resolve) => { 189 | setTimeout(() => { 190 | const blob = convertBase64ToBlob(reader.result as string) 191 | resolve(URL.createObjectURL(blob)) 192 | }, 300) 193 | }) 194 | }, 195 | }), 196 | Drawer.configure({ 197 | upload: (file: any) => { 198 | // fake upload return base 64 199 | const reader = new FileReader() 200 | reader.readAsDataURL(file) 201 | 202 | return new Promise((resolve) => { 203 | setTimeout(() => { 204 | const blob = convertBase64ToBlob(reader.result as string) 205 | resolve(URL.createObjectURL(blob)) 206 | }, 300) 207 | }) 208 | }, 209 | }), 210 | Twitter, 211 | ] 212 | 213 | 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

` 214 | 215 | function debounce(func: any, wait: number) { 216 | let timeout: NodeJS.Timeout 217 | return function (...args: any[]) { 218 | clearTimeout(timeout) 219 | // @ts-ignore 220 | timeout = setTimeout(() => func.apply(this, args), wait) 221 | } 222 | } 223 | 224 | function Editor() { 225 | const [content, setContent] = useState(DEFAULT) 226 | const [theme, setTheme] = useState('light') 227 | const [disable, setDisable] = useState(false) 228 | 229 | const onValueChange = useCallback( 230 | debounce((value: any) => { 231 | setContent(value) 232 | }, 300), 233 | [], 234 | ) 235 | 236 | return ( 237 |
242 |
248 |
256 | 257 | 258 | 259 | 260 | 261 | 264 | 265 | 272 | 279 |
280 | 281 | 291 | {bubbleDefaultDom} 292 | 293 | {extensionsNames.includes('twitter') ? : null} 297 | {extensionsNames.includes('katex') ? : null} 301 | {extensionsNames.includes('excalidraw') ? : null} 305 | {extensionsNames.includes('mermaid') ? : null} 309 | {extensionsNames.includes('drawer') ? : null} 313 | 314 | }, 315 | }} 316 | /> 317 | 318 | {typeof content === 'string' && ( 319 |