Skip to main content

Writing to the Clipboard in JavaScript

Raymond CamdenAugust 23, 2024About 6 minJavaScriptArticle(s)blogfrontendmasters.comjsjavascript

Writing to the Clipboard in JavaScript ꎀ렚

JavaScript > Article(s)

Article(s)

Writing to the Clipboard in JavaScript
The most basic use case, writing a bit of text to the user's clipboard, is mercifully easy. But there is plenty more to know. Did you know writing image data to the clipboard ONLY works with PNG?

In my last article, I showed you how to enable your website to read a visitor's clipboard. Now I'm going to follow up that guide with a look at writing to the clipboard. It goes without saying that in any use of this type of functionality, you should proceed with care and, most of all, respect for your visitors. I'll talk a bit about what that means later in the article, but for now, let's look at the API.


Before we begin


As I said last time, clipboard functionality on the web requires a “secure context”. So if you're running an http site (as opposed to an https site), these features will not work. I'd highly encourage you to get your site on https. That being said, these features, and others like them that require secure contexts, will still work on http://localhost. There's no need to set up a temporary certificate when doing local testing.


The Clipboard API

I covered this last time, but in case you didn't read the previous article in this series, the Clipboard API is supported in JavaScript via navigator.clipboard and has excellent cross-platform support:

This feature will also prompt the user for permission so remember to handle cases where they reject the request.


Writing to the Clipboard

When I last discussed the clipboard API, I mentioned how it had two APIs for reading from the clipboard, we had a readText method tailored for, you guessed it, reading text, and a more generic read method for handling complex data. Unsurprisingly, we've got the same on the write side:

And just like before, writeText is specifically for writing text to the clipboard while write gives you additional flexibility.

At the simplest, you can use it like so:

await navigator.clipboard.writeText("Hello World!");

That's literally it. Here's a Pen demonstrating an example of this, but you will most likely need to make use of the 'debug' link to see this in action due to the permissions complication of cross-domain iframes:

One pretty simple and actually practical use-case for something like this is quickly copying links to the user's clipboard. Let's consider some simple HTML:

<div class="example">
  <p>
    <a href="https://www.raymondcamden.com">My blog</a><br />
    <a href="https://frontendmasters.com/blog/">Frontend Masters blog</a>
  </p>
</div>

What I want to do is:

  1. Pick up all the links (filtered by something logical)
  2. Automatically add a UI item that will copy the URL to the clipboard
links = document.querySelectorAll("div.example p:first-child a");

links.forEach((a) => {
  let copy = document.createElement("span");
  copy.innerText = "[Copy]";
  copy.addEventListener("click", async () => {
    await navigator.clipboard.writeText(a.href);
  });
  a.after(copy);
});

I begin with a selector for the links I care about, and for each, I append a <button> element with "Copy URL of Link" as the text. Each new element has a click handler to support copying its related URL to the clipboard. As before, here's the CodePen but expect to need the debug link:

As a quick note, the UX of this demo could be improved, for example, notifying the user in some small way that the text was successfully copied.

Now let's kick it up a notch and look into how to support binary data with the write method.

The basic interface for write is to pass an array of ClipboardItem objects. The MDN docs for write provide this example:

const type = "text/plain";
const blob = new Blob([text], { type });
const data = [new ClipboardItem({ [type]: blob })];
await navigator.clipboard.write(data);

That seems sensible. For my first attempt, I decided to add simple “click to copy” support to an image. So consider this HTML:

<img src="house.jpg" alt="a house">

I could support this like so:

This code picks up the one and only image, adds a click handler, and makes use of the MDN sample code, updated to use a hard-coded type (JPG) and to fetch the binary data. (This kind of bugs me. I know the image is loaded in the DOM so it feels like I should be able to get access to the bits without another network call, but from what I can see, you can only do this by using a Canvas object.)

But when run, you get something interesting:

Uncaught (in promise) DOMException: Failed to execute 'write' on 'Clipboard': Type image/jpeg not supported on write.

What??? Turns out, this is actually documented on MDN:

Browsers commonly support writing text, HTML, and PNG image data

This seems
 crazy. I mean I definitely get having some restrictions, but it feels odd for only one type of image to be supported. However, changing my HTML:

<img src="house.png" alt="a house">

And the type in my JavaScript:

let type = 'image/png';

Confirms that it works. You can see this yourself below (and again, hit that ‘Debug' link):

So remember above when I said I didn't want to use a canvas? I did some Googling and turns out: I need to use a canvas. I found an excellent example of this on StackOverflow in this answer. In it, the author uses a temporary canvas, writes the image data to it, and uses toBlob while specifying a PNG image type. Whew. So let's see if we can build a generic solution.

First, I updated my HTML to support two images:

<div class="photos">
  <p>
    JPG:<br/>
    <img src="house.jpg" alt="a house">
  </p>
  <p>
    PNG:<br/>
    <img src="house.png" alt="a house">
  </p>
</div>

I labeled them as I was using the same picture each and needed a simple way to know which was which.

Next, I updated my code to pick up all images in that particular div:

document.addEventListener('DOMContentLoaded', init, false);

async function init() {
  let imgs = document.querySelectorAll('div.photos img');
  imgs.forEach(i => {
    i.addEventListener('click', copyImagetoCB);
  });
}

Now for the fun part. I'm going to update my code to detect the type of the image. For PNG's, it will use what I showed before, and for JPGs, I'm using a modified version of the StackOverflow answer. Here's the updated code:

You'll note that the main difference is how we get the blob. For JPGs, we're using setCanvasImage to handle the canvas shenanigans (that's what I'm calling it) and return a PNG blob.

You can see this in the CodePen below, if, again, you click out to the debug view. Note that I had to add one additional line:

img.crossOrigin = "anonymous";

As without it, you get a tainted canvas error. Sounds dirty.


With great power


It goes without saying that this API could be abused, or, more likely, used in kind of a jerky manner. So for example, if you give the user the ability to click to copy a URL, don't do sneaky crap like:

"(url here) - This URL comes from the awesome site X and you should visit it for deals!"

If you are expressing to the user that you are copying something in particular, adding to it or modifying it is simply rude. I'd even suggest that copying a short URL link instead of the original would be bad form as well.

Writing to the Clipboard in JavaScript

The most basic use case, writing a bit of text to the user's clipboard, is mercifully easy. But there is plenty more to know. Did you know writing image data to the clipboard ONLY works with PNG?