Skip to main content
Everything is accessed through a single import:
import { BubbleMenu, bubbleMenuTriggers, useBubbleMenuContext } from '@react-email/editor/ui';

Pre-built menus

Drop-in menus for common use cases. Each one handles its own trigger logic and plugin key.

BubbleMenu.Default

Text formatting toolbar that appears on text selection.
<BubbleMenu.Default />
excludeItems
ExcludableItem[]
default:"[]"
Items to hide. Options: 'bold', 'italic', 'underline', 'strike', 'code', 'uppercase', 'align-left', 'align-center', 'align-right', 'node-selector', 'link-selector'.
hideWhenActiveNodes
string[]
default:"[]"
Node types that prevent the menu from showing (e.g., 'codeBlock', 'button').
hideWhenActiveMarks
string[]
default:"[]"
Mark types that prevent the menu from showing (e.g., 'link').
placement
'top' | 'bottom'
default:"'bottom'"
Position relative to the selection.
offset
number
default:"8"
Distance from the selection in pixels.
onHide
() => void
Called when the bubble menu is hidden.

BubbleMenu.LinkDefault

Link editing menu that appears when clicking a link.
<BubbleMenu.LinkDefault />
excludeItems
('edit-link' | 'open-link' | 'unlink')[]
default:"[]"
Actions to hide from the toolbar.
placement
'top' | 'bottom'
default:"'top'"
Position relative to the link.
offset
number
Distance from the link in pixels.
onHide
() => void
Called when the bubble menu is hidden.
validateUrl
(value: string) => string | null
Custom URL validator. Return the valid URL string or null.
Called after a link is applied.
Called after a link is removed.

BubbleMenu.ButtonDefault

Button link editing menu that appears when clicking a button.
<BubbleMenu.ButtonDefault />
Same props as LinkDefault (except excludeItems).

BubbleMenu.ImageDefault

Image editing menu that appears when clicking an image.
<BubbleMenu.ImageDefault />
excludeItems
('edit-link')[]
default:"[]"
Actions to hide.
placement
'top' | 'bottom'
default:"'top'"
Position relative to the image.
offset
number
Distance from the image in pixels.
onHide
() => void
Called when the bubble menu is hidden.

Combining menus

A typical email editor uses multiple bubble menus together. Use hideWhenActiveNodes and hideWhenActiveMarks to prevent overlapping menus:
import { BubbleMenu } from '@react-email/editor/ui';

<EditorProvider extensions={extensions} content={content}>
  <BubbleMenu.Default
    hideWhenActiveNodes={['image', 'button']}
    hideWhenActiveMarks={['link']}
  />
  <BubbleMenu.LinkDefault />
  <BubbleMenu.ButtonDefault />
  <BubbleMenu.ImageDefault />
</EditorProvider>

Compound components

Build fully custom menus using the compound API.

BubbleMenu.Root

Base container for all custom bubble menus. Provides editor context to children.
import { BubbleMenu, bubbleMenuTriggers } from '@react-email/editor/ui';
import { PluginKey } from '@tiptap/pm/state';

const myPluginKey = new PluginKey('myCustomMenu');

<BubbleMenu.Root
  shouldShow={bubbleMenuTriggers.node('image')}
  pluginKey={myPluginKey}
  placement="top"
>
  {/* your custom menu content */}
</BubbleMenu.Root>
shouldShow
ShouldShowFn
Controls when the menu is visible. Defaults to showing on text selection. Use bubbleMenuTriggers for common patterns.
pluginKey
PluginKey
Unique key for the ProseMirror plugin backing this menu. Required when rendering multiple BubbleMenu.Root instances to avoid collisions. Import PluginKey from @tiptap/pm/state.
hideWhenActiveNodes
string[]
default:"[]"
Node types that prevent the menu from showing.
hideWhenActiveMarks
string[]
default:"[]"
Mark types that prevent the menu from showing.
placement
'top' | 'bottom'
default:"'bottom'"
Position relative to the selection.
offset
number
default:"8"
Distance from the selection in pixels.
onHide
() => void
Called when the bubble menu is hidden.

bubbleMenuTriggers

Factory for common shouldShow functions:
TriggerDescription
bubbleMenuTriggers.textSelection(hideNodes?, hideMarks?)Show on text selection. This is the default.
bubbleMenuTriggers.node(name)Show when a specific node type is active (e.g., 'button', 'image')
bubbleMenuTriggers.nodeWithoutSelection(name)Show when a node is active but no text is selected (e.g., 'link')

Text formatting items

ComponentDescription
BubbleMenu.ItemGroupVisual grouping of items
BubbleMenu.SeparatorDivider between groups
BubbleMenu.BoldBold toggle
BubbleMenu.ItalicItalic toggle
BubbleMenu.UnderlineUnderline toggle
BubbleMenu.StrikeStrikethrough toggle
BubbleMenu.CodeInline code toggle
BubbleMenu.UppercaseUppercase toggle
BubbleMenu.AlignLeftLeft alignment
BubbleMenu.AlignCenterCenter alignment
BubbleMenu.AlignRightRight alignment
BubbleMenu.NodeSelectorBlock type dropdown
BubbleMenu.LinkSelectorLink add/edit popover
ComponentDescription
BubbleMenu.LinkToolbarWrapper — hides when editing mode is active
BubbleMenu.LinkEditLinkButton that enters editing mode
BubbleMenu.LinkUnlinkRemoves the link
BubbleMenu.LinkOpenLinkOpens the link in a new tab
BubbleMenu.LinkFormInline form for editing link URLs

Button components

ComponentDescription
BubbleMenu.ButtonToolbarWrapper — hides when editing mode is active
BubbleMenu.ButtonEditLinkButton that enters editing mode
BubbleMenu.ButtonUnlinkRemoves the button link
BubbleMenu.ButtonFormInline form for editing button URLs

Image components

ComponentDescription
BubbleMenu.ImageToolbarWrapper — hides when editing mode is active
BubbleMenu.ImageEditLinkButton that enters editing mode

Custom menu example

Here’s a complete custom link bubble menu built from compound components:
import { BubbleMenu, bubbleMenuTriggers, useBubbleMenuContext } from '@react-email/editor/ui';
import { PluginKey } from '@tiptap/pm/state';

const linkKey = new PluginKey('myLinkMenu');

function MyLinkMenu() {
  return (
    <BubbleMenu.Root
      shouldShow={bubbleMenuTriggers.nodeWithoutSelection('link')}
      pluginKey={linkKey}
      placement="top"
    >
      <BubbleMenu.LinkToolbar>
        <BubbleMenu.LinkEditLink />
        <BubbleMenu.LinkOpenLink />
        <BubbleMenu.LinkUnlink />
      </BubbleMenu.LinkToolbar>
      <BubbleMenu.LinkForm />
    </BubbleMenu.Root>
  );
}
The LinkToolbar automatically hides when LinkEditLink is clicked, and LinkForm appears in its place. When the user submits or cancels, the toolbar reappears.

Context

Use useBubbleMenuContext() inside any child of BubbleMenu.Root to access the editor and editing state:
import { useBubbleMenuContext } from '@react-email/editor/ui';

function CustomToolbarItem() {
  const { editor, isEditing, setIsEditing } = useBubbleMenuContext();

  return (
    <button onClick={() => setIsEditing(true)}>
      Edit
    </button>
  );
}
FieldTypeDescription
editorEditorThe TipTap editor instance
isEditingbooleanWhether the menu is in editing mode
setIsEditing(value: boolean) => voidToggle editing mode

CSS import

import '@react-email/editor/styles/bubble-menu.css';
@react-email/editor/themes/default.css bundles all UI component styles. Unless you need to cherry-pick, use this single import:
import '@react-email/editor/themes/default.css';