Getting Started with Tiptap

What is Tiptap?

Tiptap is a headless rich text editor framework for the web that gives developers complete freedom to customize the editor’s look and functionality. Unlike traditional WYSIWYG editors, Tiptap provides a toolkit that allows you to build your own editor experience.

Resources

For more information and advanced usage, check out these official resources:

Common Functionality

Let’s explore some of Tiptap’s most frequently used features:

Basic Text Formatting

Tiptap makes it easy to implement common formatting options like bold, italic, underline, and strikethrough text with built-in extensions:

  • Bold: StarterKit includes the Bold extension
  • Italic: Also included in the StarterKit
  • Underline: Can be added separately with the Underline extension
  • Strike: For strikethrough text, included in StarterKit

Document Structure

  • Headings: Create hierarchical document structure with h1-h6 elements
  • Lists: Support for both ordered and unordered lists
  • Blockquotes: For quotations and callouts
  • Code blocks: For displaying formatted code snippets

Interactive Elements

  • Links: Add hyperlinks to your content
  • Images: Insert and manipulate images
  • Tables: Create and edit complex tabular data
  • Mentions: Implement @mentions for user references

Getting Started with Tiptap: A Simple Example

Let’s create a basic Tiptap editor with React to demonstrate how easy it is to get started:

Setting Up Your Project

To get started with Tiptap in a React project, follow these steps:

  1. Create a new React project (if you don’t have one already):

    1
    2
    pnpm create vite@latest tiptap-demo --template react
    cd tiptap-demo
  2. Install Tiptap and necessary extensions:

    1
    pnpm install @tiptap/react @tiptap/core @tiptap/starter-kit @tiptap/extension-underline @tiptap/extension-link
  3. Replace the contents of your App.js and create a new styles.css file with the code.

App.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import './styles.scss'

import { Color } from '@tiptap/extension-color'
import ListItem from '@tiptap/extension-list-item'
import TextStyle from '@tiptap/extension-text-style'
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'

const MenuBar = () => {
const { editor } = useCurrentEditor()

if (!editor) {
return null
}

return (
<div className="control-group">
<div className="button-group">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleBold()
.run()
}
className={editor.isActive('bold') ? 'is-active' : ''}
>
Bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleItalic()
.run()
}
className={editor.isActive('italic') ? 'is-active' : ''}
>
Italic
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleStrike()
.run()
}
className={editor.isActive('strike') ? 'is-active' : ''}
>
Strike
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={
!editor.can()
.chain()
.focus()
.toggleCode()
.run()
}
className={editor.isActive('code') ? 'is-active' : ''}
>
Code
</button>
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
Clear marks
</button>
<button onClick={() => editor.chain().focus().clearNodes().run()}>
Clear nodes
</button>
<button
onClick={() => editor.chain().focus().setParagraph().run()}
className={editor.isActive('paragraph') ? 'is-active' : ''}
>
Paragraph
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
>
H1
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
>
H2
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
>
H3
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
>
H4
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
>
H5
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
>
H6
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'is-active' : ''}
>
Bullet list
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') ? 'is-active' : ''}
>
Ordered list
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'is-active' : ''}
>
Code block
</button>
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={editor.isActive('blockquote') ? 'is-active' : ''}
>
Blockquote
</button>
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
Horizontal rule
</button>
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
Hard break
</button>
<button
onClick={() => editor.chain().focus().undo().run()}
disabled={
!editor.can()
.chain()
.focus()
.undo()
.run()
}
>
Undo
</button>
<button
onClick={() => editor.chain().focus().redo().run()}
disabled={
!editor.can()
.chain()
.focus()
.redo()
.run()
}
>
Redo
</button>
<button
onClick={() => editor.chain().focus().setColor('#958DF1').run()}
className={editor.isActive('textStyle', { color: '#958DF1' }) ? 'is-active' : ''}
>
Purple
</button>
</div>
</div>
)
}

const extensions = [
Color.configure({ types: [TextStyle.name, ListItem.name] }),
TextStyle.configure({ types: [ListItem.name] }),
StarterKit.configure({
bulletList: {
keepMarks: true,
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
},
orderedList: {
keepMarks: true,
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
},
}),
]

const content = `
<h2>
Hi there,
</h2>
<p>
this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
</p>
<ul>
<li>
That’s a bullet list with one …
</li>
<li>
… or two list items.
</li>
</ul>
<p>
Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
</p>
<pre><code class="language-css">body {
display: none;
}</code></pre>
<p>
I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.
</p>
<blockquote>
Wow, that’s amazing. Good work, boy! 👏
<br />
— Mom
</blockquote>
`

export default () => {
return (
<EditorProvider slotBefore={<MenuBar />} extensions={extensions} content={content}></EditorProvider>
)
}
  1. Run your application:
    1
    pnpm run dev

Understanding the Example

Let’s break down what’s happening in our example:

  1. Imports: We import the necessary components and extensions from Tiptap.

    • useEditor and EditorContent from @tiptap/react
    • StarterKit for essential functionality
    • Additional extensions like Underline and Link
  2. MenuBar Component: Creates a toolbar with buttons that trigger editor commands.

    • Each button uses the editor’s chain API to execute commands
    • The isActive method determines if a format is currently active
  3. TiptapEditor Component: Sets up the editor with extensions and initial content.

    • The useEditor hook initializes Tiptap
    • We define our extensions array with StarterKit and additional extensions
    • Initial content is provided as HTML
  4. CSS Styling: Basic styling to make the editor look presentable.

Extending Functionality

The example demonstrates basic functionality, but Tiptap can do much more. Here are a few ways to extend it:

1
2
3
4
5
6
7
8
9
const setLink = () => {
const url = window.prompt('URL:');

if (url) {
editor.chain().focus().setLink({ href: url }).run();
} else {
editor.chain().focus().unsetLink().run();
}
};

Implementing File Uploads

1
2
3
4
5
6
7
const addImage = () => {
const url = window.prompt('Image URL:');

if (url) {
editor.chain().focus().setImage({ src: url }).run();
}
};

Creating Custom Extensions

1
2
3
4
5
6
7
8
9
10
11
import { Extension } from '@tiptap/core';

const CustomExtension = Extension.create({
name: 'customExtension',

addKeyboardShortcuts() {
return {
'Mod-k': () => this.editor.commands.toggleBold(),
};
},
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import './styles.scss'

import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { EditorContent, useEditor } from '@tiptap/react'
import React, { useState, useEffect } from 'react'
import { WebsocketProvider } from 'y-websocket';
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import * as Y from 'yjs'

const doc = new Y.Doc();

export default function App() {
const documentId = 'test' // Replace with your actual document ID

const [provider, setProvider] = useState(null);

const randomUser = {
name: `用户${Math.random().toString(36).substring(2, 7)}`,
color: `#${Math.floor(Math.random() * 16777215).toString(16)}`
};

useEffect(() => {

// 设置 WebSocket 提供者
const wsProvider = new WebsocketProvider(
'ws://localhost:1234', // 替换为你的 WebSocket 服务器地址
`document-${documentId}`, // 确保唯一的房间名
doc
);

// 连接状态监听
wsProvider.on('status', (event) => {
console.log('WebSocket connection status:', event.status);
});

setProvider(wsProvider);

return () => {
wsProvider.destroy();
}
}, [documentId])

const editor = useEditor({
extensions: [
Document,
Paragraph,
Text,
Collaboration.configure({
document: doc,
}),
...(provider ? [CollaborationCursor.configure({
provider: provider,
user: randomUser,
})] : [])
],
}, [provider])

return <EditorContent editor={editor} />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* Basic editor styles */
.tiptap {
:first-child {
margin-top: 0;
}

/* Placeholder (at the top) */
p.is-editor-empty:first-child::before {
color: var(--gray-4);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}

p {
word-break: break-all;
}

/* Give a remote user a caret */
.collaboration-cursor__caret {
border-left: 1px solid #0d0d0d;
border-right: 1px solid #0d0d0d;
margin-left: -1px;
margin-right: -1px;
pointer-events: none;
position: relative;
word-break: normal;
}

/* Render the username above the caret */
.collaboration-cursor__label {
border-radius: 3px 3px 3px 0;
color: #0d0d0d;
font-size: 12px;
font-style: normal;
font-weight: 600;
left: -1px;
line-height: normal;
padding: 0.1rem 0.3rem;
position: absolute;
top: -1.4em;
user-select: none;
white-space: nowrap;
}
}

Getting Started with Tiptap
https://www.hardyhu.cn/2024/12/25/Getting-Started-with-Tiptap/
Author
John Doe
Posted on
December 25, 2024
Licensed under