Vue 2 reached end-of-life on December 31, 2023. Consider upgrading to Vue 3 for new projects. This package is maintained for existing Vue 2 applications.
Installation
1
Install the packages
Install the Vue 2 package along with the core package and any extensions you need:
npm install @tiptap/vue-2 @tiptap/core @tiptap/starter-kit
2
Import and use
Import the necessary components in your Vue component:
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
</script>
Core Concepts
Editor Class
In Vue 2, you create an editor instance using theEditor class directly. The editor should be created in the mounted lifecycle hook and destroyed in beforeDestroy.
Basic Usage
<template>
<div v-if="editor">
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
EditorContent Component
TheEditorContent component renders the actual editor interface.
<template>
<div class="editor-wrapper">
<editor-content
:editor="editor"
class="editor"
/>
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style scoped>
.editor-wrapper {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
</style>
Complete Working Examples
<template>
<div v-if="editor" class="container">
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit],
content: `
<h2>Hi there,</h2>
<p>this is a <em>basic</em> example of <strong>Tiptap</strong>.</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<template>
<div v-if="editor" class="container">
<div class="menu-bar">
<button
@click="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
Bold
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:disabled="!editor.can().chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
Italic
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:disabled="!editor.can().chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
Strike
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
H1
</button>
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
>
Bullet List
</button>
<button
@click="editor.chain().focus().undo().run()"
:disabled="!editor.can().chain().focus().undo().run()"
>
Undo
</button>
<button
@click="editor.chain().focus().redo().run()"
:disabled="!editor.can().chain().focus().redo().run()"
>
Redo
</button>
</div>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit],
content: `
<h2>Hi there,</h2>
<p>this is a <em>basic</em> example of <strong>Tiptap</strong>.</p>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style scoped>
.menu-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
}
button:hover {
background: #f5f5f5;
}
button.is-active {
background: #333;
color: white;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<template>
<editor-content :editor="editor" />
</template>
<script>
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { Editor, EditorContent } from '@tiptap/vue-2'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [Document, Paragraph, Text],
content: '<p>This is a minimal Tiptap editor.</p>',
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
Event Handlers
Handle editor events through the editor options:<template>
<div>
<editor-content :editor="editor" />
<div>HTML Output: {{ html }}</div>
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
html: '',
}
},
mounted() {
const vm = this
this.editor = new Editor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
onUpdate({ editor }) {
vm.html = editor.getHTML()
},
onCreate({ editor }) {
console.log('Editor created')
},
onFocus({ editor, event }) {
console.log('Editor focused')
},
onBlur({ editor, event }) {
console.log('Editor blurred')
},
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
Methods and Computed Properties
You can create methods and computed properties to interact with the editor:<template>
<div v-if="editor">
<div class="stats">
<span>Characters: {{ characterCount }}</span>
<span>Words: {{ wordCount }}</span>
</div>
<button @click="clearContent">Clear</button>
<button @click="setContent">Set Example Content</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
computed: {
characterCount() {
return this.editor?.state.doc.textContent.length || 0
},
wordCount() {
const text = this.editor?.state.doc.textContent || ''
return text.split(/\s+/).filter(Boolean).length
},
},
methods: {
clearContent() {
this.editor.commands.clearContent()
},
setContent() {
this.editor.commands.setContent('<p>This is <strong>example</strong> content!</p>')
},
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
Advanced: Custom Node Views
Create Vue 2 components as custom node views:<!-- CustomNodeComponent.vue -->
<template>
<node-view-wrapper class="custom-node">
<div class="label">Custom Vue 2 Node</div>
<node-view-content class="content" />
</node-view-wrapper>
</template>
<script>
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'
export default {
components: {
NodeViewWrapper,
NodeViewContent,
},
}
</script>
<style scoped>
.custom-node {
border: 2px solid #333;
border-radius: 8px;
padding: 1rem;
}
.label {
font-weight: bold;
margin-bottom: 0.5rem;
}
</style>
// CustomNodeExtension.js
import { Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-2'
import CustomNodeComponent from './CustomNodeComponent.vue'
export const CustomNode = Node.create({
name: 'customNode',
group: 'block',
content: 'inline*',
parseHTML() {
return [{ tag: 'div[data-type="custom-node"]' }]
},
renderHTML({ HTMLAttributes }) {
return ['div', { 'data-type': 'custom-node', ...HTMLAttributes }, 0]
},
addNodeView() {
return VueNodeViewRenderer(CustomNodeComponent)
},
})
Menus
Bubble and floating menus work with Vue 2:<template>
<div>
<editor-content :editor="editor" />
<bubble-menu :editor="editor" v-if="editor">
<button @click="editor.chain().focus().toggleBold().run()">
Bold
</button>
<button @click="editor.chain().focus().toggleItalic().run()">
Italic
</button>
</bubble-menu>
<floating-menu :editor="editor" v-if="editor">
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()">
H1
</button>
<button @click="editor.chain().focus().toggleBulletList().run()">
Bullet List
</button>
</floating-menu>
</div>
</template>
<script>
import { BubbleMenu, FloatingMenu } from '@tiptap/vue-2'
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
BubbleMenu,
FloatingMenu,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
Watchers
You can use watchers to react to editor changes:<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
import StarterKit from '@tiptap/starter-kit'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
content: '',
}
},
watch: {
content(newContent) {
// Update editor when external content changes
if (this.editor && this.editor.getHTML() !== newContent) {
this.editor.commands.setContent(newContent)
}
},
},
mounted() {
const vm = this
this.editor = new Editor({
extensions: [StarterKit],
content: this.content,
onUpdate({ editor }) {
vm.content = editor.getHTML()
},
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
Migration from Vue 2 to Vue 3
If you’re upgrading from Vue 2 to Vue 3, here are the main changes:- Vue 2
- Vue 3 (Options)
- Vue 3 (Composition)
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'
export default {
data() {
return { editor: null }
},
mounted() {
this.editor = new Editor({ /* ... */ })
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
export default {
data() {
return { editor: null }
},
mounted() {
this.editor = new Editor({ /* ... */ })
},
beforeUnmount() { // Changed from beforeDestroy
this.editor.destroy()
},
}
</script>
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
const editor = useEditor({ /* ... */ })
// Cleanup is handled automatically
</script>
Next Steps
Extensions
Explore available extensions to enhance your editor
Commands
Learn about editor commands and chains
Upgrade to Vue 3
Learn about the Vue 3 integration
Node Views
Create interactive custom nodes with Vue components