Skip to main content

Overview

The BubbleMenu component renders a contextual menu that appears when text is selected in the editor. It’s positioned using Floating UI and provides a flexible way to show formatting options or other tools near the selection.

Type Signature

const BubbleMenu: React.ForwardRefExoticComponent<
  BubbleMenuProps & React.RefAttributes<HTMLDivElement>
>

type BubbleMenuProps = Omit<BubbleMenuPluginProps, 'element'> & 
  React.HTMLAttributes<HTMLDivElement>

Props

editor
Editor | null
The editor instance. If not provided, the component will attempt to use the editor from React context via useCurrentEditor.
pluginKey
string | PluginKey
default:"'bubbleMenu'"
Unique identifier for this bubble menu plugin. Use different keys if you have multiple bubble menus.
updateDelay
number
default:"250"
Delay in milliseconds before the menu position is updated. This debounces position updates for better performance.
resizeDelay
number
default:"60"
Delay in milliseconds before the menu position is updated on window resize. This debounces resize events for better performance.
shouldShow
function | null
default:"null"
Function that determines whether the menu should be shown. Receives editor state and selection information.
shouldShow?: (props: {
  editor: Editor
  element: HTMLElement
  view: EditorView
  state: EditorState
  oldState?: EditorState
  from: number
  to: number
}) => boolean
Default behavior: Shows when there’s a non-empty text selection and the editor has focus.
appendTo
HTMLElement | (() => HTMLElement)
The DOM element to append the menu to. Useful when you need to render the menu in a specific container for z-index or overflow reasons.Default: The editor’s parent element.
getReferencedVirtualElement
() => VirtualElement | null
Function that returns a custom virtual element for positioning. Useful for custom positioning logic.Default: Positions based on the current selection.
options
object
Floating UI configuration options for positioning and behavior.
children
React.ReactNode
required
The content to render inside the bubble menu.
...props
React.HTMLAttributes<HTMLDivElement>
All standard HTML div attributes (className, style, etc.) are supported.

Usage Examples

Basic Usage

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

function MyEditor() {
  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>
    </>
  )
}

With Custom Styling

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

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

  return (
    <>
      <EditorContent editor={editor} />
      <BubbleMenu
        editor={editor}
        className="bubble-menu"
        style={{
          background: '#ffffff',
          border: '1px solid #ccc',
          borderRadius: '8px',
          padding: '8px',
          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
        }}
      >
        <button onClick={() => editor?.chain().focus().toggleBold().run()}>
          Bold
        </button>
        <button onClick={() => editor?.chain().focus().toggleItalic().run()}>
          Italic
        </button>
      </BubbleMenu>
    </>
  )
}

Custom shouldShow Logic

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

function MyEditor() {
  const editor = useEditor({
    extensions: [StarterKit, Link],
    content: '<p>Select a <a href="#">link</a> to see options</p>',
  })

  return (
    <>
      <EditorContent editor={editor} />
      <BubbleMenu
        editor={editor}
        shouldShow={({ editor, from, to }) => {
          // Only show when a link is selected
          return editor.isActive('link')
        }}
      >
        <button onClick={() => editor?.chain().focus().unsetLink().run()}>
          Remove Link
        </button>
        <button onClick={() => {
          const url = window.prompt('URL')
          if (url) {
            editor?.chain().focus().setLink({ href: url }).run()
          }
        }}>
          Edit Link
        </button>
      </BubbleMenu>
    </>
  )
}

Multiple Bubble Menus

import { useEditor, EditorContent, BubbleMenu } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'

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

  return (
    <>
      <EditorContent editor={editor} />
      
      {/* Text formatting menu */}
      <BubbleMenu
        editor={editor}
        pluginKey="textMenu"
        shouldShow={({ editor }) => {
          return !editor.isActive('link') && !editor.isActive('image')
        }}
      >
        <button onClick={() => editor?.chain().focus().toggleBold().run()}>
          Bold
        </button>
        <button onClick={() => editor?.chain().focus().toggleItalic().run()}>
          Italic
        </button>
      </BubbleMenu>
      
      {/* Link menu */}
      <BubbleMenu
        editor={editor}
        pluginKey="linkMenu"
        shouldShow={({ editor }) => editor.isActive('link')}
      >
        <button onClick={() => editor?.chain().focus().unsetLink().run()}>
          Remove Link
        </button>
      </BubbleMenu>
      
      {/* Image menu */}
      <BubbleMenu
        editor={editor}
        pluginKey="imageMenu"
        shouldShow={({ editor }) => editor.isActive('image')}
      >
        <button onClick={() => editor?.chain().focus().deleteSelection().run()}>
          Remove Image
        </button>
      </BubbleMenu>
    </>
  )
}

With Active States

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

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

  if (!editor) {
    return null
  }

  return (
    <>
      <EditorContent editor={editor} />
      <BubbleMenu editor={editor} className="bubble-menu">
        <button
          onClick={() => editor.chain().focus().toggleBold().run()}
          className={editor.isActive('bold') ? 'active' : ''}
        >
          Bold
        </button>
        <button
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={editor.isActive('italic') ? 'active' : ''}
        >
          Italic
        </button>
        <button
          onClick={() => editor.chain().focus().toggleStrike().run()}
          className={editor.isActive('strike') ? 'active' : ''}
        >
          Strike
        </button>
      </BubbleMenu>
    </>
  )
}

With Custom Positioning

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

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

  return (
    <>
      <EditorContent editor={editor} />
      <BubbleMenu
        editor={editor}
        options={{
          placement: 'bottom',
          offset: 16,
          flip: { padding: 8 },
          shift: { padding: 8 },
        }}
      >
        <button onClick={() => editor?.chain().focus().toggleBold().run()}>
          Bold
        </button>
      </BubbleMenu>
    </>
  )
}

Styling

CSS Example

.bubble-menu {
  display: flex;
  gap: 4px;
  padding: 8px;
  background: white;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.bubble-menu button {
  padding: 6px 12px;
  border: none;
  background: transparent;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s;
}

.bubble-menu button:hover {
  background: #f1f5f9;
}

.bubble-menu button.active {
  background: #3b82f6;
  color: white;
}

Important Notes

The BubbleMenu uses Floating UI for positioning, which provides intelligent placement that avoids overflow and adjusts to viewport constraints.
When clicking on buttons inside the bubble menu, make sure to use mousedown or include .focus() in your command chains to prevent the editor from losing focus.
Use different pluginKey values when rendering multiple bubble menus to avoid conflicts. Each menu needs a unique identifier.

Default Behavior

By default, the bubble menu:
  • Shows when text is selected
  • Hides when the selection is empty
  • Hides when the editor loses focus (unless focus moves to an element inside the menu)
  • Positions itself above the selection
  • Automatically adjusts position when the viewport is too small

Build docs developers (and LLMs) love