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 >
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.
Delay in milliseconds before the menu position is updated. This debounces position updates for better performance.
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.
Floating UI configuration options for positioning and behavior. strategy
'absolute' | 'fixed'
default: "'absolute'"
Positioning strategy for the menu.
Preferred placement of the menu relative to the selection. Options: 'top', 'right', 'bottom', 'left', or any -start/-end variant.
offset
number | object | boolean
default: "8"
Offset from the reference element in pixels. Can be a number, detailed offset configuration, or false to disable.
flip
object | boolean
default: "{}"
Enable flipping to the opposite placement if thereβs not enough space. Pass an object for configuration or false to disable.
shift
object | boolean
default: "{}"
Enable shifting along the axis to keep the menu in view. Pass an object for configuration or false to disable.
arrow
object | false
default: "false"
Configuration for an arrow element pointing to the reference. Pass an object with element reference or false to disable.
size
object | boolean
default: "false"
Enable size adjustments to fit the menu within the viewport. Pass an object for configuration or false to disable.
autoPlacement
object | boolean
default: "false"
Automatically choose the placement with the most space. Pass an object for configuration or false to disable.
hide
object | boolean
default: "false"
Hide the menu when the reference is hidden or has escaped the boundary. Pass an object for configuration or false to disable.
inline
object | boolean
default: "false"
Improve positioning for inline reference elements. Pass an object for configuration or false to disable.
scrollTarget
HTMLElement | Window
default: "window"
The scrollable element to listen to for scroll events. The menu position will update when this element is scrolled.
Callback function called when the menu is shown.
Callback function called when the menu is hidden.
Callback function called when the menu position is updated.
Callback function called when the menu is destroyed.
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 >
</>
)
}
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 : 4 px ;
padding : 8 px ;
background : white ;
border : 1 px solid #e2e8f0 ;
border-radius : 8 px ;
box-shadow : 0 4 px 6 px rgba ( 0 , 0 , 0 , 0.1 );
}
.bubble-menu button {
padding : 6 px 12 px ;
border : none ;
background : transparent ;
border-radius : 4 px ;
cursor : pointer ;
transition : background 0.2 s ;
}
.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