> ## Documentation Index
> Fetch the complete documentation index at: https://react.email/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Image Upload

> Upload images via paste, drop, or slash command with the image upload plugin.

## Quick start

Import `useEditorImage`, add the returned extension to your editor, register
`imageSlashCommand`, and render `BubbleMenu.ImageDefault` for inline editing.

```tsx theme={"theme":{"light":"github-light","dark":"vesper"}}
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](/editor/api-reference/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:

```tsx theme={"theme":{"light":"github-light","dark":"vesper"}}
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.

```tsx theme={"theme":{"light":"github-light","dark":"vesper"}}
<EditorProvider extensions={[StarterKit, imageExtension]}>
  <BubbleMenu hideWhenActiveNodes={['image']} />
  <BubbleMenu.ImageDefault />
</EditorProvider>
```

To expose image uploads from `/`, add `imageSlashCommand` to the slash command
items:

```tsx theme={"theme":{"light":"github-light","dark":"vesper"}}
<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:

```tsx theme={"theme":{"light":"github-light","dark":"vesper"}}
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:

```tsx theme={"theme":{"light":"github-light","dark":"vesper"}}
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:

<CardGroup cols={2}>
  <Card title="Image Upload" icon="code" href="https://react.email/editor/examples/image-upload">
    Paste, drop, and slash-command image upload with a stubbed uploader.
  </Card>
</CardGroup>
