Redactor supports several ways to add images to a document — file upload, URL, image library, drag-and-drop, and clipboard paste. This guide walks through each method and the options that control them.
For the full list of image.* settings, see Image settings.
Ways to insert an image#
| Method | Option | UI |
|---|---|---|
| Upload files | image.upload |
Upload tab in the image dropdown |
| Paste a URL | image.url |
By URL tab (or a standalone URL form) |
| Pick from a library | image.select |
Select tab with thumbnails |
| Drag files into the editor | image.upload + image.drop |
No extra UI |
| Paste from clipboard | image.upload + image.clipboard |
No extra UI |
Click the Image button in the toolbar or add bar to open the dropdown. Which tabs appear depends on what you enable.
Insert by URL#
image.url is on by default. Users can paste a direct image URL (https://…/photo.jpg) in the By URL tab and click Insert. No file is uploaded — the URL is stored as src.
If URL is the only insert source (upload and select are off), Redactor opens a compact URL form instead of a tabbed dropdown.
Upload to your server#
Point image.upload at your backend endpoint:
const app = Redactor('#entry', {
image: {
upload: '/upload/image'
}
});
Redactor sends a POST with multipart/form-data. The default field name is file (image.name).
Server response
The endpoint must return JSON.
Single image:
{
"url": "/uploads/photo.jpg"
}
Multiple images — object with arbitrary keys, or an array:
{
"file-0": { "url": "/uploads/a.jpg", "id": "abc" },
"file-1": { "url": "/uploads/b.jpg", "id": "def" }
}
Each item needs url or src.
Optional fields: id, alt, caption, width, height, srcset, link.
Error — any object with a truthy error property:
{
"error": true,
"message": "File type not allowed"
}
Field name and extra data
const app = Redactor('#entry', {
image: {
upload: '/upload/image',
name: 'file',
data: {
folder: 'articles',
token: 'csrf-token-here'
}
}
});
image.data fields are merged into the FormData before send. Use this for CSRF tokens, folder ids, or tenant ids.
When image.multiple is true (default), files are sent as file[]. When false, the first file is still sent as file[] with one entry.
Allowed file types
Restrict the file picker with image.types:
const app = Redactor('#entry', {
image: {
upload: '/upload/image',
types: ['image/jpeg', 'image/png', 'image/webp']
}
});
Default: ['image/*'].
Image library (Select)#
Load thumbnails from your server or from inline JSON:
const app = Redactor('#entry', {
image: {
select: '/api/images.json'
}
});
Or preload the list:
const app = Redactor('#entry', {
image: {
select: [
{ thumb: '/thumb.jpg', url: '/full.jpg', alt: 'Photo' }
]
}
});
Each entry needs url (full size) and optionally thumb (grid preview).
Also supported: id, alt, caption, link, width, height.
HTTP method for a remote endpoint: image.selectMethod ('get' or 'post', default 'get').
Drag and drop#
When image.drop is true (default) and image.upload is set, users can drag image files from the desktop into the editor. Redactor uploads them the same way as the Upload tab.
Drop onto an existing image block to replace that image.
Set drop: false to disable file drops. Drag-and-drop still requires image.upload — URL or base64 mode.
const app = Redactor('#entry', {
image: {
upload: '/upload/image',
drop: false
}
});
Cancel a drop from code:
app.on('image:before:drop', (payload) => {
if (payload.files.length > 10) payload.cancel = true;
});
Clipboard paste#
With image.clipboard: true (default) and image.upload enabled:
- Image files from the clipboard (screenshots, copied files) are uploaded and inserted as image blocks.
- HTML with
data:image/…URLs (Word, Pages, browsers) is processed before insert:- Server upload mode — each data URL is uploaded;
srcis replaced with the returned URL. base64mode — data URLs are kept as-is.
- Server upload mode — each data URL is uploaded;
When image.upload is off, pasted HTML with images is still inserted — existing src values are preserved.
const app = Redactor('#entry', {
image: {
upload: '/upload/image',
clipboard: false
}
});
Base64 mode (no server)#
Store images inline as data URLs — useful for demos, offline editors, or small documents:
const app = Redactor('#entry', {
image: {
upload: 'base64',
convertFormat: 'image/webp',
convertQuality: 0.8
}
});
No HTTP request is made. Files are converted client-side to data:image/webp;base64,… (or another convertFormat). Drag-and-drop and clipboard file paste work the same way.
Supported convertFormat values: image/webp, image/jpeg, image/png, image/avif.
Custom upload handler#
Replace the built-in POST with your own logic — custom headers, chunked upload, cloud SDK, etc.:
const app = Redactor('#entry', {
image: {
upload(uploader, { data, files, e }) {
fetch('/api/upload', {
method: 'POST',
headers: { 'X-Auth-Token': '…' },
body: data
})
.then((res) => res.json())
.then((json) => uploader.complete(json, e))
.catch((err) => uploader.complete({ error: true, message: String(err) }, e));
}
}
});
data—FormDatawith files already appended underimage.namefiles—FileListfrom input, drop, or paste- Call
uploader.complete(response, e)when done — same response shape as the server endpoint
Hook into the request before send:
app.on('upload:before:send', ({ data, files }) => {
data.append('source', 'redactor');
return { data, files };
});
Return { cancel: true } to abort.
Replace the image UI entirely#
image.create and image.edit replace the built-in dropdown with your own picker or modal:
const app = Redactor('#entry', {
image: {
create(app) {
openMyMediaLibrary((url) => {
app.image.insert({ url }, 'insert');
});
},
edit(app, block) {
openMyMediaLibrary((url) => {
block.update({ src: url });
});
}
}
});
Upload, URL, and Select tabs are skipped when image.create is set.
Single file only#
Disable multi-select in the file picker and limit drag-and-drop to one file per action:
const app = Redactor('#entry', {
image: {
upload: '/upload/image',
multiple: false
}
});
Default is true — users can pick or drop several images at once.
Track added and removed images#
When users delete images from a document, you may need to remove the files from storage. Redactor tracks image references through the trackedRefs module.
Enable tracking on init:
const app = Redactor('#entry', {
trackedRefs: {
images: true
},
image: {
upload: '/upload/image'
}
});
true tracks img[src]. To track another attribute (for example a storage key in data-id):
trackedRefs: {
images: { key: 'data-id' }
}
On editor:ready, Redactor snapshots the initial image keys. Before save, read the diff:
document.querySelector('#save').addEventListener('click', () => {
const diff = app.trackedRefs.getDiff();
if (!diff.hasBaseline) return;
// URLs (or data-id values) no longer in the document
diff.images.removed.forEach((key) => {
fetch('/api/delete-image', {
method: 'POST',
body: JSON.stringify({ url: key })
});
});
// New images since load
console.log('Added:', diff.images.added);
saveArticle(app.getHtml());
});
getDiff() returns:
{
hasBaseline: true,
images: {
enabled: true,
initial: ['/uploads/a.jpg'],
current: ['/uploads/b.jpg'],
added: ['/uploads/b.jpg'],
removed: ['/uploads/a.jpg']
},
links: { /* same shape when link tracking is enabled */ }
}
Reset the baseline after a successful save so the next diff is relative to the saved state:
app.trackedRefs.captureBaseline();
API reference: trackedRefs.
Events#
| Event | When |
|---|---|
image:insert |
Image block inserted (upload, URL, select) |
image:upload |
Existing image block updated via upload |
image:upload:error |
Upload failed in the image dropdown |
upload:success / upload:error |
Any upload through the uploader service |
image:before:drop |
Before file drop — set cancel: true to abort |
app.on('image:insert', ({ block, data }) => {
console.log('Inserted:', data.src || data.url);
});
app.on('upload:error', ({ response }) => {
alert(response?.message || 'Upload failed');
});