# How to Add Custom Bubble Menu Actions for Specific Node Types in Tiptap

> Learn how to add custom bubble menu actions for specific node types in Tiptap. Extend BubbleOptions and implement button view factories to customize your editor experience.

- Repository: [Hung Hoang/reactjs-tiptap-editor](https://github.com/hunghg255/reactjs-tiptap-editor)
- Tags: how-to-guide
- Published: 2026-03-03

---

**You can add custom bubble menu actions by extending the `BubbleOptions` configuration in your node extension, defining a unique token in the `list` array, and implementing a corresponding `button` view factory that returns the menu item component and action handler.**

The `reactjs-tiptap-editor` repository implements a data-driven bubble menu system that maps node types to menu items through a declarative configuration API. By leveraging the `BubbleOptions` interface exported from [`src/components/Bubble/formatBubble.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/components/Bubble/formatBubble.ts), you can inject custom actions into the floating toolbar that appears when users select specific nodes like images or videos.

## Understanding the Bubble Menu Architecture

The bubble menu system in this editor is purely functional and configuration-based rather than hard-coded. It relies on a type-safe mapping between node types and their corresponding toolbar items.

### Core Types and Configuration Files

The architecture centers on three key definitions in [`src/components/Bubble/formatBubble.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/components/Bubble/formatBubble.ts):

- **`BubbleAllType`** (lines 30-36): A union type representing every possible bubble menu item identifier, including built-in actions like `image-size-small` and extension names.
- **`NodeTypeMenu`** (lines 42-45): A mapped type `Partial<Record<NodeTypeKey, BubbleAllType[]>>` that associates specific node types with arrays of menu item tokens.
- **`BubbleOptions`** (lines 66-75): The public API consumed by extensions, containing the `list` (a `NodeTypeMenu`), `defaultBubbleList`, and a `button` factory function that creates concrete menu item objects.

### Runtime Rendering Pipeline

When a user selects content, [`src/components/Bubble/RichTextBubbleMedia.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/components/Bubble/RichTextBubbleMedia.tsx) (lines 54-86) detects the active node type and calls the appropriate helper function—such as `getBubbleImage` or `getBubbleVideo`—defined in [`formatBubble.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/formatBubble.ts). These helpers iterate over the tokens specified in `options.list` for that node type, invoke the `button` factory to resolve each token into a `BubbleMenuItem` object, and pass the resulting configuration to the UI renderer.

## Step-by-Step Implementation

Adding a custom action requires extending your node extension's options to declare the new menu item and provide its implementation.

### 1. Define a Unique Token Constant

Create a type-safe constant to identify your custom action. This token must be assignable to `BubbleAllType`.

```typescript
// src/constants/customBubble.ts
export const IMAGE_WATERMARK = 'image-add-watermark' as const;

```

### 2. Extend the Node Extension Configuration

In your node extension file (for example, [`src/extensions/Image/Image.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/extensions/Image/Image.ts)), modify the `addOptions` method to register the token in the `list` mapping and provide a `button` factory implementation.

```typescript
// src/extensions/Image/Image.ts
import { IMAGE_WATERMARK } from '@/constants/customBubble';
import { ActionButton } from '@/components';
import { BubbleOptions } from '@/components/Bubble/formatBubble';

export const Image = TiptapImage.extend({
  addOptions(): BubbleOptions<any> {
    return {
      // ... existing options
      list: {
        image: [
          'image-size-small',
          'image-align-center',
          IMAGE_WATERMARK, // ← Your custom token appended to the array
        ],
      },
      defaultBubbleList: [],
      button: ({ editor, t }) => ({
        // Return a map keyed by your custom token
        [IMAGE_WATERMARK]: {
          type: IMAGE_WATERMARK,
          component: ActionButton,
          componentProps: {
            tooltip: t('editor.watermark.tooltip'),
            icon: 'Watermark',
            action: () => {
              const attrs = editor.getAttributes('image');
              editor
                .chain()
                .focus()
                .updateImage({
                  ...attrs,
                  src: `${attrs.src}?watermark=1`,
                })
                .run();
            },
          },
        },
      }),
    };
  },
});

```

### 3. Implement the Action Logic

The `action` property inside `componentProps` receives the Tiptap editor instance. Use standard Tiptap commands to manipulate the node. The example above appends a query parameter to the image source, but you can implement complex logic such as opening modals, triggering API calls, or chaining multiple editor commands.

### 4. Create a Custom UI Component (Optional)

If the default `ActionButton` component does not meet your design requirements, create a custom React component and reference it in the `component` property.

```tsx
// src/components/WatermarkButton.tsx
import { FC } from 'react';
import { IconComponent } from '@/components/icons';

export const WatermarkButton: FC<{ action: () => void; tooltip: string }> = ({
  action,
  tooltip,
}) => (
  <button
    onClick={action}
    className="richtext-bubble-menu-btn"
    title={tooltip}
    type="button"
  >
    <IconComponent name="Watermark" className="richtext-size-4" />
  </button>
);

```

Then update the extension configuration:

```typescript
import { WatermarkButton } from '@/components/WatermarkButton';

// Inside button factory:
[IMAGE_WATERMARK]: {
  type: IMAGE_WATERMARK,
  component: WatermarkButton,
  componentProps: {
    action: () => { /* ... */ },
    tooltip: 'Add Watermark',
  },
}

```

## Complete Working Example

Here is the full configuration for adding a watermark action to the Image extension:

```typescript
// src/extensions/Image/Image.ts
import TiptapImage from '@tiptap/extension-image';
import { IMAGE_WATERMARK } from '@/constants/customBubble';
import { ActionButton } from '@/components';
import { BubbleOptions } from '@/components/Bubble/formatBubble';

export const Image = TiptapImage.extend({
  addOptions(): BubbleOptions<any> {
    return {
      list: {
        image: [
          'image-size-small',
          'image-align-center',
          IMAGE_WATERMARK,
        ],
      },
      defaultBubbleList: [],
      button: ({ editor, t }) => ({
        [IMAGE_WATERMARK]: {
          type: IMAGE_WATERMARK,
          component: ActionButton,
          componentProps: {
            tooltip: t('editor.image.watermark'),
            icon: 'Watermark',
            action: () => {
              const attrs = editor.getAttributes('image');
              if (!attrs.src) return;
              
              editor
                .chain()
                .focus()
                .updateImage({ 
                  src: `${attrs.src}?watermark=1` 
                })
                .run();
            },
          },
        },
      }),
    };
  },
});

```

Ensure the extension is included in your editor configuration:

```typescript
import { useEditor } from '@tiptap/react';
import { Image } from '@/extensions/Image';

const editor = useEditor({
  extensions: [
    Image,
    // ... other extensions
  ],
});

```

## Summary

- The bubble menu system uses a **declarative configuration** where node extensions declare their menu items via `BubbleOptions`.
- **`NodeTypeMenu`** maps node types to arrays of string tokens defined in [`src/components/Bubble/formatBubble.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/components/Bubble/formatBubble.ts).
- The **`button` factory** in your extension's `addOptions` translates tokens into concrete UI components by returning objects with `component`, `componentProps`, and `action` properties.
- **[`RichTextBubbleMedia.tsx`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/RichTextBubbleMedia.tsx)** automatically picks up custom items when they are listed in the `list` configuration for the active node type.
- You can reuse the built-in **`ActionButton`** component or provide custom React components for specialized UI requirements.

## Frequently Asked Questions

### Can I add bubble menu items to multiple node types at once?

Yes. The `list` property in `BubbleOptions` accepts a `NodeTypeMenu` object where you can map the same token to multiple node keys. For example, you could add a `custom-download` token to both `image` and `video` arrays. However, you must ensure the extension providing the `button` factory is responsible for both node types, or configure the button factory in a shared extension that handles the token resolution for all relevant nodes.

### How do I remove or override default bubble menu items?

To override defaults, omit the built-in tokens from the `list` array in your extension's `BubbleOptions` and replace them with your custom implementations. The `defaultBubbleList` property can also be used to filter out unwanted items globally. If you need to modify built-in behavior, extend the original extension and override the `addOptions` method to return a modified `list` array containing only your desired tokens.

### Can I use a custom React component instead of ActionButton?

Absolutely. The `component` property in the object returned by the `button` factory accepts any React component. Your custom component receives the `componentProps` object as props, which includes `action`, `tooltip`, `icon`, and any other properties you define. Create your component in `src/components/`, import it into your extension file, and reference it instead of `ActionButton` in the configuration.

### Why isn't my custom bubble menu item appearing?

First, verify that your token string in the `list` array exactly matches the key returned by the `button` factory (including case sensitivity). Second, ensure your extension is properly registered in the editor's extensions array. Finally, check that the node type under the cursor matches the key in your `NodeTypeMenu` (e.g., `image` vs `tiptapImage`). The `RichTextBubbleMedia` component only renders items when the current selection matches a configured node type in [`src/components/Bubble/formatBubble.ts`](https://github.com/hunghg255/reactjs-tiptap-editor/blob/main/src/components/Bubble/formatBubble.ts).