Skip to main content
The BubbleMenu extension provides a floating menu that appears when text is selected in the editor. It’s positioned intelligently using Floating UI and supports custom positioning, visibility rules, and framework-specific components for React and Vue.

Installation

npm install @tiptap/extension-bubble-menu

Basic Usage

Vanilla JavaScript

import { Editor } from '@tiptap/core'
import BubbleMenu from '@tiptap/extension-bubble-menu'

const menu = document.querySelector('#bubble-menu')

const editor = new Editor({
  extensions: [
    BubbleMenu.configure({
      element: menu,
    }),
  ],
})

React

import { BubbleMenu, useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function Editor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Select some text to see the bubble menu</p>',
  })

  return (
    <>
      <EditorContent editor={editor} />
      <BubbleMenu editor={editor}>
        <button onClick={() => editor.chain().focus().toggleBold().run()}>
          Bold
        </button>
        <button onClick={() => editor.chain().focus().toggleItalic().run()}>
          Italic
        </button>
      </BubbleMenu>
    </>
  )
}

Vue

<template>
  <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>
</template>

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

export default {
  components: {
    EditorContent,
    BubbleMenu,
  },
  data() {
    return {
      editor: null,
    }
  },
  mounted() {
    this.editor = new Editor({
      extensions: [StarterKit],
      content: '<p>Select some text to see the bubble menu</p>',
    })
  },
  beforeUnmount() {
    this.editor.destroy()
  },
}
</script>

Configuration Options

element
HTMLElement
required
The DOM element that contains your menu.Default: null
pluginKey
PluginKey | string
The plugin key for the bubble menu.Default: 'bubbleMenu'
updateDelay
number
The delay in milliseconds before the menu position should be updated. This can be useful to prevent performance issues.Default: 250
resizeDelay
number
The delay in milliseconds before the menu position should be updated on window resize.Default: 60
shouldShow
function
A function that determines whether the menu should be shown or not. If this function returns false, the menu will be hidden, otherwise it will be shown.Parameters:
  • editor: The editor instance
  • element: The menu element
  • view: The ProseMirror EditorView
  • state: The current editor state
  • oldState: The previous editor state (optional)
  • from: Selection start position
  • to: Selection end position
Default: Shows when text is selected and editor has focus
BubbleMenu.configure({
  element: menu,
  shouldShow: ({ editor, view, state, from, to }) => {
    // Only show for text selections
    return from !== to && !state.selection.empty
  },
})
appendTo
HTMLElement | (() => HTMLElement)
The DOM element to append your menu to. Sometimes the menu needs to be appended to a different DOM context due to accessibility, clipping, or z-index issues.Default: Editor’s parent element
BubbleMenu.configure({
  element: menu,
  appendTo: () => document.body,
})
getReferencedVirtualElement
() => VirtualElement | null
A function that returns the virtual element for the menu. This is useful when the menu needs to be positioned relative to a specific DOM element.
BubbleMenu.configure({
  element: menu,
  getReferencedVirtualElement: () => ({
    getBoundingClientRect: () => customElement.getBoundingClientRect(),
  }),
})
options
object
Configuration options passed to Floating UI for positioning. See Floating UI documentation for full details.Properties:
options.strategy
'absolute' | 'fixed'
Positioning strategy.Default: 'absolute'
options.placement
string
Menu placement relative to selection. Options: 'top', 'right', 'bottom', 'left', 'top-start', 'top-end', 'right-start', 'right-end', 'bottom-start', 'bottom-end', 'left-start', 'left-end'Default: 'top'
options.offset
number | object | boolean
Distance in pixels from the selection.Default: 8
BubbleMenu.configure({
  element: menu,
  options: {
    offset: 16,
  },
})
options.flip
object | boolean
Whether to flip the menu to the opposite side if there’s not enough space.Default: {}
options.shift
object | boolean
Whether to shift the menu along the axis to keep it in view.Default: {}
options.scrollTarget
HTMLElement | Window
The scrollable element that should be listened to when updating the position of the bubble menu.Default: window
BubbleMenu.configure({
  element: menu,
  options: {
    scrollTarget: document.querySelector('.scroll-container'),
  },
})
options.onShow
() => void
Callback fired when the menu is shown.
options.onHide
() => void
Callback fired when the menu is hidden.
options.onUpdate
() => void
Callback fired when the menu position is updated.
options.onDestroy
() => void
Callback fired when the menu is destroyed.

React Component Props

When using the React component, you can pass these props:
editor
Editor
The editor instance. Can be omitted if used inside EditorProvider.
pluginKey
string
The plugin key.Default: 'bubbleMenu'
updateDelay
number
Update delay in milliseconds.
resizeDelay
number
Resize delay in milliseconds.
shouldShow
function
Function to determine visibility.
options
object
Floating UI configuration options.
Plus all standard HTML div attributes (className, style, etc.).

Vue Component Props

editor
Editor
required
The editor instance.
pluginKey
string | Object
The plugin key.Default: 'bubbleMenu'
updateDelay
number
Update delay in milliseconds.
resizeDelay
number
Resize delay in milliseconds.
shouldShow
function
Function to determine visibility.
options
object
Floating UI configuration options.
appendTo
HTMLElement | Function
Element to append the menu to.
getReferencedVirtualElement
function
Function to get the virtual element for positioning.

Advanced Examples

Custom Visibility Logic

BubbleMenu.configure({
  element: menu,
  shouldShow: ({ editor, state, from, to }) => {
    // Only show bubble menu for links
    return editor.isActive('link')
  },
})

Update Position Programmatically

// Trigger a position update
editor.view.dispatch(
  editor.state.tr.setMeta('bubbleMenu', 'updatePosition')
)

Multiple Bubble Menus

function Editor() {
  const editor = useEditor({
    extensions: [StarterKit, Link],
  })

  return (
    <>
      <EditorContent editor={editor} />
      
      {/* Text formatting menu */}
      <BubbleMenu
        editor={editor}
        pluginKey="textMenu"
        shouldShow={({ editor, from, to }) => {
          return from !== to && !editor.isActive('link')
        }}
      >
        <button onClick={() => editor.chain().focus().toggleBold().run()}>
          Bold
        </button>
        <button onClick={() => editor.chain().focus().toggleItalic().run()}>
          Italic
        </button>
      </BubbleMenu>
      
      {/* Link-specific menu */}
      <BubbleMenu
        editor={editor}
        pluginKey="linkMenu"
        shouldShow={({ editor }) => editor.isActive('link')}
      >
        <button onClick={() => editor.chain().focus().unsetLink().run()}>
          Remove Link
        </button>
      </BubbleMenu>
    </>
  )
}

Custom Positioning

BubbleMenu.configure({
  element: menu,
  options: {
    placement: 'bottom',
    offset: 20,
    flip: {
      fallbackPlacements: ['top', 'right', 'left'],
    },
    shift: {
      padding: 8,
    },
  },
})

Source Code

View the source code on GitHub:

Build docs developers (and LLMs) love