Overview
The BubbleMenu component renders a contextual menu that appears near the user’s text selection. It’s commonly used for formatting toolbars that appear when text is selected, similar to Medium or Google Docs.
Signature
const BubbleMenu: DefineComponent<{
editor: Editor
pluginKey?: string | PluginKey
updateDelay?: number
resizeDelay?: number
options?: TippyOptions
appendTo?: Element | (() => Element) | 'parent'
shouldShow?: (props: {
editor: Editor
view: EditorView
state: EditorState
oldState?: EditorState
from: number
to: number
}) => boolean
getReferencedVirtualElement?: (props: {
editor: Editor
view: EditorView
state: EditorState
oldState?: EditorState
from: number
to: number
}) => DOMRect | Range | VirtualElement
}>
Props
The Tiptap editor instance. This prop is required.
pluginKey
string | PluginKey
default:"'bubbleMenu'"
A unique key for the ProseMirror plugin. Use this if you need multiple bubble menus or want to identify this specific plugin.
updateDelay
number
default:"undefined"
Delay in milliseconds before updating the menu position after a selection change. Useful for debouncing rapid selection changes.
resizeDelay
number
default:"undefined"
Delay in milliseconds before updating the menu position after a window resize event.
Configuration options for the underlying Tippy.js positioning library. Use this to customize the menu’s placement, offset, animation, and other display properties.Common options:
placement - Position relative to reference (e.g., ‘top’, ‘bottom’, ‘left’, ‘right’)
offset - Distance from the reference element
duration - Animation duration
zIndex - CSS z-index value
appendTo
Element | (() => Element) | 'parent'
default:"undefined"
The element to append the bubble menu to. Can be:
- A DOM element
- A function that returns a DOM element
- The string
'parent' to append to the editor’s parent element
By default, the menu is appended to document.body.
A callback function that determines whether the bubble menu should be visible. Receives an object with:
editor - The editor instance
view - The ProseMirror EditorView
state - Current editor state
oldState - Previous editor state
from - Start position of selection
to - End position of selection
Return true to show the menu, false to hide it.By default, the menu shows when there’s a text selection and hides when the selection is empty.
getReferencedVirtualElement
function
default:"undefined"
A function that returns a custom reference element for positioning. Use this to position the menu relative to a specific element or virtual element instead of the text selection.Receives the same props as shouldShow and should return a DOMRect, Range, or Tippy.js VirtualElement.
Lifecycle
- onMounted: Registers the bubble menu plugin with the editor and sets up positioning
- onBeforeUnmount: Unregisters the plugin from the editor
Styling
The component inherits attributes (inheritAttrs: false is set, but attributes are manually applied), allowing you to add custom classes, styles, or other HTML attributes:
<BubbleMenu :editor="editor" class="custom-bubble-menu" style="background: white;">
<!-- content -->
</BubbleMenu>
Examples
Basic Usage
<script setup>
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Select some text to see the bubble menu!</p>',
extensions: [
StarterKit,
],
})
</script>
<template>
<div>
<EditorContent :editor="editor" />
<BubbleMenu :editor="editor" v-if="editor">
<button @click="editor.chain().focus().toggleBold().run()">
Bold
</button>
<button @click="editor.chain().focus().toggleItalic().run()">
Italic
</button>
</BubbleMenu>
</div>
</template>
<script setup>
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Select text to format</p>',
extensions: [StarterKit],
})
</script>
<template>
<div>
<EditorContent :editor="editor" />
<BubbleMenu :editor="editor" v-if="editor" class="bubble-menu">
<button
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
Bold
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
Italic
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
Strike
</button>
</BubbleMenu>
</div>
</template>
<style scoped>
.bubble-menu {
display: flex;
gap: 0.25rem;
padding: 0.25rem;
background-color: white;
border: 1px solid #ccc;
border-radius: 0.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.bubble-menu button {
padding: 0.25rem 0.5rem;
border: none;
background: transparent;
cursor: pointer;
border-radius: 0.25rem;
}
.bubble-menu button:hover {
background-color: #f0f0f0;
}
.bubble-menu button.is-active {
background-color: #000;
color: #fff;
}
</style>
Custom shouldShow Logic
<script setup>
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
const editor = useEditor({
content: '<p>Select a link to see the bubble menu</p>',
extensions: [
StarterKit,
Link,
],
})
// Only show the bubble menu when a link is selected
function shouldShow({ editor, from, to }) {
return from !== to && editor.isActive('link')
}
</script>
<template>
<div>
<EditorContent :editor="editor" />
<BubbleMenu
:editor="editor"
v-if="editor"
:shouldShow="shouldShow"
>
<button @click="editor.chain().focus().unsetLink().run()">
Remove Link
</button>
<button @click="openLinkEditor">
Edit Link
</button>
</BubbleMenu>
</div>
</template>
With Tippy Options
<script setup>
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Select text</p>',
extensions: [StarterKit],
})
const tippyOptions = {
placement: 'top',
duration: 100,
offset: [0, 10],
zIndex: 1000,
}
</script>
<template>
<div>
<EditorContent :editor="editor" />
<BubbleMenu
:editor="editor"
v-if="editor"
:options="tippyOptions"
>
<div class="menu-content">
<button @click="editor.chain().focus().toggleBold().run()">
Bold
</button>
</div>
</BubbleMenu>
</div>
</template>
<script setup>
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
const editor = useEditor({
content: '<p>Different bubble menus for different contexts</p>',
extensions: [StarterKit, Link, Image],
})
function shouldShowTextMenu({ editor, from, to }) {
return from !== to && !editor.isActive('link') && !editor.isActive('image')
}
function shouldShowLinkMenu({ editor }) {
return editor.isActive('link')
}
function shouldShowImageMenu({ editor }) {
return editor.isActive('image')
}
</script>
<template>
<div>
<EditorContent :editor="editor" />
<!-- Text formatting menu -->
<BubbleMenu
:editor="editor"
v-if="editor"
pluginKey="textMenu"
:shouldShow="shouldShowTextMenu"
>
<button @click="editor.chain().focus().toggleBold().run()">
Bold
</button>
<button @click="editor.chain().focus().toggleItalic().run()">
Italic
</button>
</BubbleMenu>
<!-- Link menu -->
<BubbleMenu
:editor="editor"
v-if="editor"
pluginKey="linkMenu"
:shouldShow="shouldShowLinkMenu"
>
<button @click="editor.chain().focus().unsetLink().run()">
Remove Link
</button>
</BubbleMenu>
<!-- Image menu -->
<BubbleMenu
:editor="editor"
v-if="editor"
pluginKey="imageMenu"
:shouldShow="shouldShowImageMenu"
>
<button @click="editor.chain().focus().deleteSelection().run()">
Delete Image
</button>
</BubbleMenu>
</div>
</template>
Notes
- The bubble menu is automatically positioned near the text selection using Tippy.js
- By default, it shows only when there’s a non-empty text selection
- The component renders as a
<div> element that you can style with classes or inline styles
- Multiple bubble menus can coexist by using different
pluginKey values
- The menu is removed from the DOM and re-parented by the plugin when shown
- Use
shouldShow to customize when the menu appears based on editor state
See Also