Skip to main content
Tiptap provides support for Vue 2 applications. While Vue 2 has reached end-of-life, this integration allows legacy projects to use Tiptap.
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 the Editor 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

The EditorContent 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>

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)
  },
})
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:
<script>
import { Editor, EditorContent } from '@tiptap/vue-2'

export default {
  data() {
    return { editor: null }
  },
  mounted() {
    this.editor = new Editor({ /* ... */ })
  },
  beforeDestroy() {
    this.editor.destroy()
  },
}
</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