One challenge I encountered when creating was copying the raw bytes of a PNG image to the clipboard. The current best practice for doing this is to use the . This API isn't supported on all browsers, so I'll also show you how to use the ClipBoard API and a legacy implementation for browser compatibility. Then we'll create a complete implementation that defaults to the ClipBoard API and falls back to the legacy implementation. Spindle ClipBoard API Loading an Image as a Binary Blob The implementation of Spindle uses a python back-end that implements a RESTful API. The API takes some arguments about to generate and sends back the raw bytes of a PNG file. In the Spindle front-end code, I use the JavaScript Fetch API to interact with the back-end API. To mimic this scenario for our example, we're going to load an image from our local machine using the Fetch API. The code to do this looks like the following. blob = await (await fetch( "./static/test-image.png", )).blob() This code does a couple of things. Reads a local file using fetch Extracts the blob The is just a block of binary data with some type metadata. blob There are some oddities using fetch with local files. To do this without running into Cross-Origin Resource Sharing (CORS) issues, we need to setup a local nginx server and point it at our code. Here's a one-liner to do that using docker. $ docker run -d --name nginx-test -v $(pwd):/usr/share/nginx/html -p 80:80 nginx Then, whenever you make changes, you can use the following reload your app. $ docker restart nginx-test I wrote a separate post about that you can checkout for more details. running NGINX locally using Docker that includes an auto-reload script The PNG blob that you get from fetch isn't in the right format to use it as an . To actually view the image, I have a helper function. This code converts our raw blob into a base64 url that plays nicely with . img.src img function blobToBase64(blob) { return new Promise((resolve, _) => { const reader = new FileReader(); reader.onloadend = function (event) { const result = event.target.result resolve(result.replace(/^data:.+;base64,/, '')) } reader.readAsDataURL(blob); }); } Here's the full code to load and view our image. To read data from an API like I do for Spindle, you just need to change the fetch call. JavaScript function blobToBase64(blob) { return new Promise((resolve, _) => { const reader = new FileReader(); reader.onloadend = function (event) { const result = event.target.result resolve(result.replace(/^data:.+;base64,/, '')) } reader.readAsDataURL(blob); }); } window.onload = async function registerCallbacks() { // Grab the element where we want to display our image const image = document.getElementById("imageToCopy"); // Load the PNG blog we want to show blob = await (await fetch( "./static/test-image.png", )).blob() // Set our image src using the image data encoded in base64 image.src = "data:image/png;base64," + await blobToBase64(blob); } HTML <img id="imageToCopy" /> The ClipBoard API The ClipBoard API has two main benefits: it provides and access to the clipboard. The Object-Oriented approach makes the clipboard easy to think about and visualize. The object is, intuitively, an item that you want to place on the clipboard. Async support is helpful because it ensures that our API calls are , which means that copying our large image blob won't lock up the browser. object-oriented asynchronous ClipboardItem non-blocking Let's get started with the code. Since we already have our blob, this part is pretty simple. It's just matter of using the API. All the ClipBoard API needs is the blob and it's type which the blob already knows in ! blob.type async function clipboardAPICopy(blob) { await navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob }) ]); } The Legacy execCommand API The ClipBoard API isn't supported on all browsers. Even in browsers that do support it, users can disable it anyway. The ClipBoard API is also not supported over HTTP. Since the API isn't guaranteed to be available, we need to implement a fallback. The interface is a legacy interface that is supported on any reasonable browser your code might need to run on. We don't want to default to it though because it's a blocking interface. means that it will stop all interactivity in your user's browser while it executes. For small snippets of text, this is probably not noticeable. When copying an image, like we're trying to do, the larger data size can lead to a noticeable blip in your website's responsiveness. execCommand Blocking Using the interface feels a little convoluted. It makes more sense if you think about it in two steps: execCommand Highlighting a set of elements in the DOM Performing an operation on those selected elements First, we want to clear out any current selections let selection = window.getSelection(); selection.removeAllRanges(); Next, we create a selection with the element we want to copy. image range.selectNode(image); selection.addRange(range); Finally, we use to our selection and clean up. execCommand copy document.execCommand("copy"); selection.removeAllRanges(); Here's one function to wrap that all up. We want both the function and the function to take a as an argument. To accomplish this for , we create a temporary, invisible element, set the source, and delete it. We could use the element we already set up to view the image, but I want an unified interface between our two different copy functions. clipboardAPICopy execCommandCopy Blob execCommandCopy image image async function execCommandCopy(blob) { image = document.createElement("img"); image.style = "display: none;" image.src = "data:image/png;base64," + await blobToBase64(blob); document.body.appendChild(image); let selection = window.getSelection(); selection.removeAllRanges(); let range = document.createRange(); range.selectNode(image); selection.addRange(range); document.execCommand("copy"); selection.removeAllRanges(); document.body.removeChild(image); } Pulling It All Together Now, we have all of the components that we need to implement our desired interface: one function that will work on any browser. Here's the code with some console messages to report which API we're using to do the copy. copyBlobToClipBoard async function copyBlobToClipBoard(blob) { try { await clipboardAPICopy(blob); console.log("Image copied using the ClipBoard API!"); } catch ({name, _}) { console.log("Recieved " + name + " when accessing the ClipBoard API. Falling back to execCommand"); await execCommandCopy(blob); console.log("Image copied using the execCommand API!"); } } Here's a minimal HTML page implementing everything we've talked about. <!DOCTYPE html> <html lang=en-US> <head> <meta charset="UTF-8" /> <script> function blobToBase64(blob) { return new Promise((resolve, _) => { const reader = new FileReader(); reader.onloadend = function (event) { const result = event.target.result resolve(result.replace(/^data:.+;base64,/, '')) } reader.readAsDataURL(blob); }); } async function clipboardAPICopy(blob) { await navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob }) ]); } async function execCommandCopy(blob) { image = document.createElement("img"); image.style = "display: none;" image.src = "data:image/png;base64," + await blobToBase64(blob); document.body.appendChild(image); let selection = window.getSelection(); selection.removeAllRanges(); let range = document.createRange(); range.selectNode(image); selection.addRange(range); document.execCommand("copy"); selection.removeAllRanges(); document.body.removeChild(image); } async function copyBlobToClipBoard(blob) { try { await clipboardAPICopy(blob); console.log("Image copied using the ClipBoard API!"); } catch ({name, _}) { console.log("Recieved " + name + " when accessing the ClipBoard API. Falling back to execCommand"); await execCommandCopy(blob); console.log("Image copied using the execCommand API!"); } } window.onload = async function registerCallbacks() { const image = document.getElementById("imageToCopy"); blob = await (await fetch( "./static/test-image.png", )).blob() image.src = "data:image/png;base64," + await blobToBase64(blob); image.addEventListener("click", async () => await copyBlobToClipBoard(blob)); }; </script> </head> <body> <img id="imageToCopy" /> </body> </html> To show both methods working, we can use two different browsers: FireFox and Chrome. Chrome implements the ClipBoard API by default, but FireFox does not. Running in chrome, we will see the following in the console. Then in FireFox, your console should look like this. We did it! We've taken a PNG blob and copied it to the clipboard in a widely compatible way 🥳 Originally published . here