Skip to main content

Quick start

Import useEditorImage, add the returned extension to your editor, register imageSlashCommand, and render BubbleMenu.ImageDefault for inline editing.
import { StarterKit } from '@react-email/editor/extensions';
import { imageSlashCommand, useEditorImage } from '@react-email/editor/plugins';
import {
  BubbleMenu,
  defaultSlashCommands,
  SlashCommand,
} from '@react-email/editor/ui';
import { EditorProvider } from '@tiptap/react';
import { useCallback } from 'react';
import '@react-email/editor/themes/default.css';

export function MyEditor() {
  const uploadImage = useCallback(async (file: File) => {
    const url = await uploadToStorage(file);
    return { url };
  }, []);

  const imageExtension = useEditorImage({ uploadImage });

  return (
    <EditorProvider extensions={[StarterKit, imageExtension]}>
      <BubbleMenu.ImageDefault />
      <SlashCommand.Root items={[...defaultSlashCommands, imageSlashCommand]} />
    </EditorProvider>
  );
}
uploadImage receives a File and must resolve with { url }. The returned URL is written to the image node once the promise resolves. See Image Upload API for the hook, types, slash command, and editor commands exposed by this plugin.

How image upload works

The image upload plugin combines an image node extension with a ProseMirror file-handler plugin:
  • useEditorImage({ uploadImage }) creates the extension and keeps the latest upload handler wired in.
  • Paste and drop events are intercepted when the first file is an image.
  • editor.commands.uploadImage() opens a file picker for image/*.
  • All entry points run the same upload flow: insert a temporary blob URL, await uploadImage(file), then swap the node to the final hosted URL.
If uploadImage throws, the temporary image node is removed and the error is logged, which keeps failed uploads from lingering in the document.

Using EmailEditor

EmailEditor wraps the same extension behind a single prop. Use this when you don’t need direct access to the extension or slash command list:
import { EmailEditor } from '@react-email/editor';

export function MyEditor() {
  return (
    <EmailEditor
      onUploadImage={async (file) => ({ url: await uploadToStorage(file) })}
    />
  );
}
When onUploadImage is present, EmailEditor enables the same upload flow for paste, drop, and image insertion.

Bubble menus and slash commands

When pairing BubbleMenu.ImageDefault with the default BubbleMenu, pass hideWhenActiveNodes={['image']} so the text menu steps aside when an image is focused.
<EditorProvider extensions={[StarterKit, imageExtension]}>
  <BubbleMenu hideWhenActiveNodes={['image']} />
  <BubbleMenu.ImageDefault />
</EditorProvider>
To expose image uploads from /, add imageSlashCommand to the slash command items:
<SlashCommand.Root items={[...defaultSlashCommands, imageSlashCommand]} />

Upload triggers

Once the extension is registered, three input paths upload automatically:
  • Paste — paste an image from the clipboard
  • Drop — drag an image file onto the editor
  • Slash command — type / and pick Image (from imageSlashCommand)
All three run the same flow: a temporary blob URL renders while uploadImage runs, and the node swaps to the resolved URL on success.

Programmatic insertion

The extension adds two editor commands:
editor.commands.uploadImage();

editor.commands.setImage({
  src: 'https://example.com/hero.png',
  alt: 'Hero image',
  alignment: 'center',
});
uploadImage() opens a file picker and runs the upload flow. setImage() inserts an image node directly, which is useful when you already have a URL. Available setImage attributes: src, alt, width, height, alignment ('left' | 'center' | 'right'), and href (wraps the image in a link on export).

Error handling

If uploadImage throws, the plugin removes the temporary node for you and logs the failure via console.error. Handle the error inside your own function when you need custom UI or telemetry:
const uploadImage = useCallback(async (file: File) => {
  try {
    const url = await uploadToStorage(file);
    return { url };
  } catch (error) {
    toast.error(`Couldn't upload ${file.name}`);
    throw error;
  }
}, []);

Examples

See image upload in action with a runnable example:

Image Upload

Paste, drop, and slash-command image upload with a stubbed uploader.