Upload images

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; src is replaced with the returned URL.
    • base64 mode — data URLs are kept as-is.

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 — FormData with files already appended under image.name
  • files — FileList from 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');
});