Paste-Image-From-Clipboard Handler

When a user hits Cmd-V with a screenshot in their clipboard, we want to upload it as if they had drag-dropped. This is the 30-line paste handler I keep, including the Safari-only `clipboardData.items` traversal and a graceful HEIC fallback.

JavaScript
Frontend
2 snippets
js-dom
frontend
code-template
rajreeves

By @rajreeves

January 14, 2026

·

Updated August 10, 2026

957 views

28

4.2 (10)

// Listen on the document so any focused element triggers the paste,
// or scope to a specific element by passing it in. We ignore plain-text pastes
// (handled by the textarea natively) and only act on image MIME types.

function onPasteImages(target, onImages) {
    function handle(e) {
        const items = e.clipboardData && e.clipboardData.items;
        if (!items || items.length === 0) return;
        const images = [];
        for (let i = 0; i < items.length; i++) {
            const item = items[i];
            if (item.kind === 'file' && item.type.startsWith('image/')) {
                const file = item.getAsFile();
                if (file) images.push(file);
            }
        }
        if (images.length === 0) return;
        // Calling preventDefault stops the default paste-as-text fallback,
        // which would otherwise insert the image's filename into a textarea.
        e.preventDefault();
        onImages(images);
    }
    target.addEventListener('paste', handle);
    return () => target.removeEventListener('paste', handle);
}

// Drive it with a synthetic event because the playground has no real clipboard.
const listeners = [];
const fakeEl = {
    addEventListener(name, fn) { listeners.push([name, fn]); },
    removeEventListener() {},
};
const fakePngFile = { name: 'image.png', type: 'image/png', size: 4096 };
const fakeTextItem = { kind: 'string', type: 'text/plain', getAsFile: () => null };
const fakeImageItem = { kind: 'file', type: 'image/png', getAsFile: () => fakePngFile };

onPasteImages(fakeEl, (imgs) => {
    console.log('pasted', imgs.length, 'image(s):', imgs.map((f) => f.name));
});

const pasteEvent = {
    preventDefault() { console.log('preventDefault called'); },
    clipboardData: { items: [fakeTextItem, fakeImageItem] },
};
listeners[0][1](pasteEvent);

// And a no-image paste does nothing.
listeners[0][1]({ preventDefault() { console.log('should not appear'); }, clipboardData: { items: [fakeTextItem] } });

The whole pattern hinges on clipboardData.items rather than clipboardData.files, because Safari does not populate files on paste events even when an image is on the clipboard. Walking items and filtering on kind === 'file' plus type.startsWith('image/') works in every desktop browser I have tested. The preventDefault call is what stops the textarea from inserting image.png as plain text, which is the most common bug report: "I pasted a screenshot and nothing happened, but the filename appeared." Returning the cleanup function lets React or any framework remove the listener on unmount.