Files
resend__react-email/apps/docs/editor/api-reference/ui/bubble-menu.mdx

310 lines
8.5 KiB
Plaintext

---
title: "BubbleMenu"
sidebarTitle: "BubbleMenu"
description: "Floating contextual toolbars for text, links, images, and buttons."
icon: "message-lines"
---
Everything is accessed through a single import:
```tsx
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.LinkDefault
Link editing menu that appears when clicking a link.
```tsx
<BubbleMenu.LinkDefault />
```
<ResponseField name="excludeItems" type="('edit-link' | 'open-link' | 'unlink')[]" default="[]">
Actions to hide from the toolbar.
</ResponseField>
<ResponseField name="placement" type="'top' | 'bottom'" default="'top'">
Position relative to the link.
</ResponseField>
<ResponseField name="offset" type="number">
Distance from the link in pixels.
</ResponseField>
<ResponseField name="onHide" type="() => void">
Called when the bubble menu is hidden.
</ResponseField>
<ResponseField name="validateUrl" type="(value: string) => string | null">
Custom URL validator. Return the valid URL string or `null`.
</ResponseField>
<ResponseField name="onLinkApply" type="(href: string) => void">
Called after a link is applied.
</ResponseField>
<ResponseField name="onLinkRemove" type="() => void">
Called after a link is removed.
</ResponseField>
### BubbleMenu.ButtonDefault
Button link editing menu that appears when clicking a button.
```tsx
<BubbleMenu.ButtonDefault />
```
Same props as `LinkDefault` (except `excludeItems`).
### BubbleMenu.ImageDefault
Image editing menu that appears when clicking an image.
```tsx
<BubbleMenu.ImageDefault />
```
<ResponseField name="excludeItems" type="('edit-link' | 'unlink')[]" default="[]">
Actions to hide from the toolbar.
</ResponseField>
<ResponseField name="placement" type="'top' | 'bottom'" default="'top'">
Position relative to the image.
</ResponseField>
<ResponseField name="offset" type="number">
Distance from the image in pixels.
</ResponseField>
<ResponseField name="onHide" type="() => void">
Called when the bubble menu is hidden.
</ResponseField>
<ResponseField name="validateUrl" type="(value: string) => string | null">
Custom URL validator. Return the valid URL string or `null`.
</ResponseField>
<ResponseField name="onLinkApply" type="(href: string) => void">
Called after a link is applied to the image.
</ResponseField>
<ResponseField name="onLinkRemove" type="() => void">
Called after a link is removed from the image.
</ResponseField>
---
## Combining menus
A typical email editor uses multiple bubble menus together. Use `hideWhenActiveNodes` and `hideWhenActiveMarks`
to prevent overlapping menus:
```tsx
import { BubbleMenu } from '@react-email/editor/ui';
<EditorProvider extensions={extensions} content={content}>
<BubbleMenu
hideWhenActiveNodes={['image', 'button']}
hideWhenActiveMarks={['link']}
/>
<BubbleMenu.LinkDefault />
<BubbleMenu.ButtonDefault />
<BubbleMenu.ImageDefault />
</EditorProvider>
```
---
## Compound components
Build fully custom menus using the compound API.
### BubbleMenu
Base container for all custom bubble menus. Provides editor context to children.
When rendered without children, it automatically renders the default text formatting toolbar.
```tsx
import { BubbleMenu, bubbleMenuTriggers } from '@react-email/editor/ui';
import { PluginKey } from '@tiptap/pm/state';
const myPluginKey = new PluginKey('myCustomMenu');
<BubbleMenu
trigger={bubbleMenuTriggers.node('image')}
pluginKey={myPluginKey}
placement="top"
>
{/* your custom menu content */}
</BubbleMenu>
```
<ResponseField name="trigger" type="TriggerFn">
Controls when the menu is visible. Defaults to showing on text selection.
Use `bubbleMenuTriggers` for common patterns.
</ResponseField>
<ResponseField name="pluginKey" type="PluginKey">
Unique key for the ProseMirror plugin backing this menu. Required when
rendering multiple `BubbleMenu` instances to avoid collisions.
Import `PluginKey` from `@tiptap/pm/state`.
</ResponseField>
<ResponseField name="hideWhenActiveNodes" type="string[]" default="[]">
Node types that prevent the menu from showing.
</ResponseField>
<ResponseField name="hideWhenActiveMarks" type="string[]" default="[]">
Mark types that prevent the menu from showing.
</ResponseField>
<ResponseField name="placement" type="'top' | 'bottom'" default="'bottom'">
Position relative to the selection.
</ResponseField>
<ResponseField name="offset" type="number" default="8">
Distance from the selection in pixels.
</ResponseField>
<ResponseField name="onHide" type="() => void">
Called when the bubble menu is hidden.
</ResponseField>
### bubbleMenuTriggers
Factory for common `trigger` functions:
| Trigger | Description |
| -- | -- |
| `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
| Component | Description |
| -- | -- |
| `BubbleMenu.ItemGroup` | Visual grouping of items |
| `BubbleMenu.Separator` | Divider between groups |
| `BubbleMenu.Bold` | Bold toggle |
| `BubbleMenu.Italic` | Italic toggle |
| `BubbleMenu.Underline` | Underline toggle |
| `BubbleMenu.Strike` | Strikethrough toggle |
| `BubbleMenu.Code` | Inline code toggle |
| `BubbleMenu.Uppercase` | Uppercase toggle |
| `BubbleMenu.AlignLeft` | Left alignment |
| `BubbleMenu.AlignCenter` | Center alignment |
| `BubbleMenu.AlignRight` | Right alignment |
| `BubbleMenu.NodeSelector` | Block type dropdown |
| `BubbleMenu.LinkSelector` | Link add/edit popover |
### Link components
| Component | Description |
| -- | -- |
| `BubbleMenu.LinkToolbar` | Wrapper -- hides when editing mode is active |
| `BubbleMenu.LinkEditLink` | Button that enters editing mode |
| `BubbleMenu.LinkUnlink` | Removes the link |
| `BubbleMenu.LinkOpenLink` | Opens the link in a new tab |
| `BubbleMenu.LinkForm` | Inline form for editing link URLs |
### Button components
| Component | Description |
| -- | -- |
| `BubbleMenu.ButtonToolbar` | Wrapper -- hides when editing mode is active |
| `BubbleMenu.ButtonEditLink` | Button that enters editing mode |
| `BubbleMenu.ButtonUnlink` | Removes the button link |
| `BubbleMenu.ButtonForm` | Inline form for editing button URLs |
### Image components
| Component | Description |
| -- | -- |
| `BubbleMenu.ImageToolbar` | Wrapper -- hides when editing mode is active |
| `BubbleMenu.ImageEditLink` | Button that enters editing mode |
| `BubbleMenu.ImageUnlink` | Removes the link wrapping the image |
| `BubbleMenu.ImageForm` | Inline form for editing the image's link |
---
## Custom menu example
Here's a complete custom link bubble menu built from compound components:
```tsx
import { BubbleMenu, bubbleMenuTriggers, useBubbleMenuContext } from '@react-email/editor/ui';
import { PluginKey } from '@tiptap/pm/state';
const linkKey = new PluginKey('myLinkMenu');
function MyLinkMenu() {
return (
<BubbleMenu
trigger={bubbleMenuTriggers.nodeWithoutSelection('link')}
pluginKey={linkKey}
placement="top"
>
<BubbleMenu.LinkToolbar>
<BubbleMenu.LinkEditLink />
<BubbleMenu.LinkOpenLink />
<BubbleMenu.LinkUnlink />
</BubbleMenu.LinkToolbar>
<BubbleMenu.LinkForm />
</BubbleMenu>
);
}
```
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` to access the editor and editing state:
```tsx
import { useBubbleMenuContext } from '@react-email/editor/ui';
function CustomToolbarItem() {
const { editor, isEditing, setIsEditing } = useBubbleMenuContext();
return (
<button onClick={() => setIsEditing(true)}>
Edit
</button>
);
}
```
| Field | Type | Description |
| -- | -- | -- |
| `editor` | `Editor` | The TipTap editor instance |
| `isEditing` | `boolean` | Whether the menu is in editing mode |
| `setIsEditing` | `(value: boolean) => void` | Toggle editing mode |
---
## CSS import
```tsx
import '@react-email/editor/styles/bubble-menu.css';
```
<Tip>
`@react-email/editor/themes/default.css` bundles all UI component styles.
Unless you need to cherry-pick, use this single import:
```tsx
import '@react-email/editor/themes/default.css';
```
</Tip>