Skip to main content
Tiptap provides native Vue 3 support with composables and components that work seamlessly with the Composition API.

Installation

1

Install the packages

Install the Vue 3 package along with the core package and any extensions you need:
npm install @tiptap/vue-3 @tiptap/core @tiptap/starter-kit
2

Import and use

Import the necessary components and composables in your Vue component:
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
</script>

Core Concepts

useEditor Composable

The useEditor composable is the primary way to create and manage an editor instance in Vue 3. It returns a shallow ref containing the editor instance.

Type Signature

function useEditor(
  options?: Partial<EditorOptions>
): Ref<Editor | undefined>

Basic Usage with Composition API

<template>
  <editor-content :editor="editor" />
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})
</script>

Options API Usage

You can also use the Editor class directly with the Options API:
<template>
  <editor-content v-if="editor" :editor="editor" />
</template>

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

  beforeUnmount() {
    this.editor.destroy()
  },
}
</script>

EditorContent Component

The EditorContent component renders the actual editor interface.
interface EditorContentProps {
  editor: Editor | null
}

Usage Example

<template>
  <div class="editor-wrapper">
    <editor-content 
      :editor="editor" 
      class="prose max-w-none"
    />
  </div>
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})
</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">
    <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().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>
    </div>
    
    <editor-content :editor="editor" />
  </div>
</template>

<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
  extensions: [StarterKit],
  content: `
    <h2>Hi there,</h2>
    <p>this is a <em>basic</em> example of <strong>Tiptap</strong>.</p>
  `,
})
</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>

Event Handlers

Handle editor events through the editor options:
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { ref } from 'vue'

const html = ref('')

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
  
  onUpdate: ({ editor }) => {
    html.value = editor.getHTML()
  },
  
  onCreate: ({ editor }) => {
    console.log('Editor created')
  },
  
  onFocus: ({ editor, event }) => {
    console.log('Editor focused')
  },
  
  onBlur: ({ editor, event }) => {
    console.log('Editor blurred')
  },
})
</script>

<template>
  <div>
    <editor-content :editor="editor" />
    <div>HTML Output: {{ html }}</div>
  </div>
</template>

Reactivity

The editor instance is a shallow ref, so you need to use .value when accessing it in script:
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { watch } from 'vue'

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})

// Watch editor changes
watch(editor, (newEditor) => {
  if (newEditor) {
    console.log('Editor is ready')
  }
})

// Access editor in functions
function getContent() {
  return editor.value?.getHTML()
}
</script>

Advanced: Custom Node Views

Create Vue components as custom node views:
<!-- CustomNodeComponent.vue -->
<template>
  <node-view-wrapper class="custom-node">
    <div class="label">Custom Vue Node</div>
    <node-view-content class="content" />
  </node-view-wrapper>
</template>

<script setup>
import { NodeViewWrapper, NodeViewContent, nodeViewProps } from '@tiptap/vue-3'

defineProps(nodeViewProps)
</script>

<style scoped>
.custom-node {
  border: 2px solid #333;
  border-radius: 8px;
  padding: 1rem;
}

.label {
  font-weight: bold;
  margin-bottom: 0.5rem;
}
</style>
// CustomNodeExtension.ts
import { Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'
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 seamlessly with Vue 3:
<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 setup>
import { BubbleMenu, FloatingMenu } from '@tiptap/vue-3'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})
</script>

TypeScript Support

Tiptap has full TypeScript support:
<script setup lang="ts">
import type { Editor } from '@tiptap/core'
import type { Ref } from 'vue'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'

const editor: Ref<Editor | undefined> = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})
</script>

Provide/Inject Pattern

Share the editor instance across components using Vue’s provide/inject:
<!-- Parent.vue -->
<template>
  <div>
    <Toolbar />
    <editor-content :editor="editor" />
  </div>
</template>

<script setup>
import { provide } from 'vue'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Toolbar from './Toolbar.vue'

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})

provide('editor', editor)
</script>
<!-- Toolbar.vue -->
<template>
  <div v-if="editor" class="toolbar">
    <button @click="editor.chain().focus().toggleBold().run()">
      Bold
    </button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

const editor = inject('editor')
</script>

Next Steps

Extensions

Explore available extensions to enhance your editor

Commands

Learn about editor commands and chains

Styling

Customize the editor’s appearance

Node Views

Create interactive custom nodes with Vue components