mirror of
https://github.com/resend/react-email.git
synced 2026-09-14 14:18:02 +08:00
310 lines
8.5 KiB
Plaintext
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>
|