mirror of
https://github.com/resend/react-email.git
synced 2026-09-14 14:18:02 +08:00
07bf7ad569
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
162 lines
4.7 KiB
Plaintext
162 lines
4.7 KiB
Plaintext
---
|
|
title: "Image Upload"
|
|
sidebarTitle: "Image Upload"
|
|
description: "Upload images via paste, drop, or slash command with the image upload plugin."
|
|
icon: "image"
|
|
---
|
|
|
|
## Quick start
|
|
|
|
Import `useEditorImage`, add the returned extension to your editor, register
|
|
`imageSlashCommand`, and render `BubbleMenu.ImageDefault` for inline editing.
|
|
|
|
```tsx
|
|
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
|
|
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
|
|
<EditorProvider extensions={[StarterKit, imageExtension]}>
|
|
<BubbleMenu hideWhenActiveNodes={['image']} />
|
|
<BubbleMenu.ImageDefault />
|
|
</EditorProvider>
|
|
```
|
|
|
|
To expose image uploads from `/`, add `imageSlashCommand` to the slash command
|
|
items:
|
|
|
|
```tsx
|
|
<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
|
|
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
|
|
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>
|