Async Clipboard API

Safari 13.1 adds support for the async Clipboard API. This API allows you, as web developers, to write and read data to and from the system clipboard, and offers several advantages over the current state of the art for reading and writing clipboard data via DataTransfer. Let’s take a look at how this new API works.

The API

The Clipboard API introduces two new objects:

  • Clipboard, which is accessible through navigator.clipboard and contains methods for reading from and writing to the system clipboard.
  • ClipboardItem, which represents a single item on the system clipboard that may have multiple representations. Currently, when reading and writing data, WebKit supports four MIME type representations: "text/plain", "text/html", "text/uri-list", and "image/png".

Conceptually, the Clipboard is represented by an ordered list of one or more ClipboardItem, each of which may have multiple type representations. For instance, when writing several PNG images to the clipboard, you should create a ClipboardItem for each image, with a single "image/png" type representation for each item. When writing a single PNG image with some alt text, you would instead write a single ClipboardItem with two representations: "image/png" and "text/plain".

You can use clipboard.read to extract data from the system clipboard; this asynchronously retrieves an array of ClipboardItem, each containing a mapping of MIME type to Blob. Similarly, clipboard.write can be used to write a given array of ClipboardItem to the system clipboard. However, if you are only trying to read or write plain text, you may find the methods clipboard.readText and clipboard.writeText to be more ergonomic.

Each ClipboardItem also has a presentationStyle, which may indicate whether the item is best represented as inline data or an “attachment” (that is, a file-like entity). This distinction may be useful in order to tell a copied text selection on a webpage apart from a copied HTML file.

Let’s dive into some examples below to see how to programmatically read and write data.

Writing Data

Consider this basic example, which implements a button that copies plain text when clicked:

<button id="new-copy">Copy text</button>
<script>
document.getElementById("new-copy").addEventListener("click", event => {
    navigator.clipboard.writeText("This text was copied programmatically.");
});
</script>

This is much simpler than the current method of programmatically copying text, which requires us to select a text field and execute the “copy” command:

<button id="old-copy">Copy text</button>
<script>
document.getElementById("old-copy").addEventListener("click", event => {
    let input = document.createElement("input");
    input.style.opacity = "0";
    input.style.position = "fixed";
    input.value = "This text was also copied programmatically.";
    document.body.appendChild(input);

    input.focus();
    input.setSelectionRange(0, input.value.length);
    document.execCommand("Copy");

    input.remove();
});
</script>

When using clipboard.write to copy data, you need to create an array of ClipboardItem. Each ClipboardItem is initialized with a mapping of MIME type to Promise which may resolve either to a string or a Blob of the same MIME type. The following example uses clipboard.write to copy a single item with both plain text and HTML representations.

<button id="copy-html">Copy text and markup</button>
<div>Then paste in the box below:</div>
<div contenteditable spellcheck="false" style="width: 200px; height: 100px; overflow: hidden; border: 1px solid black;"></div>
<script>
document.getElementById("copy-html").addEventListener("click", event => {
    navigator.clipboard.write([
        new ClipboardItem({
            "text/plain": Promise.resolve("This text was copied using `Clipboard.prototype.write`."),
            "text/html": Promise.resolve("<p style='color: red; font-style: oblique;'>This text was copied using <code>Clipboard.prototype.write</code>.</p>"),
        }),
    ]);
});
</script>

A similar implementation using existing DataTransfer API would require us to create a hidden text field, install a copy event handler on the text field, focus it, trigger a programmatic copy, set data on the DataTransfer (within the copy event handler), and finally call preventDefault.

Note that both clipboard.write and clipboard.writeText are asynchronous. If you attempt to write to the clipboard while a prior clipboard writing invocation is still pending, the previous invocation will immediately reject, and the new content will be written to the clipboard.

On both iOS and macOS, the order in which types are written to the clipboard is also important. WebKit writes data to the system pasteboard in the order specified — this means types that come before other types are considered by the system to have “higher fidelity” (that is, preserve more of the original content). Native apps on macOS and iOS may use this fidelity order as a hint when choosing an appropriate UTI (universal type identifier) to read.

Reading Data

Data extraction follows a similar flow. In the following example, we:

  1. Use clipboard.read to obtain a list of clipboard items.
  2. Resolve the first item’s "text/html" data to a Blob using clipboardItem.getType.
  3. Use the FileReader API to read the contents of the Blob as text.
  4. Display the pasted markup by setting the innerHTML of a container <div>.
<span style="font-weight: bold; background-color: black; color: white;">Select this text and copy</span>
<div><button id="read-html">Paste HTML below</button></div>
<div id="html-output"></div>
<script>
document.getElementById("read-html").addEventListener("click", async clickEvent => {
    let items = await navigator.clipboard.read();
    for (let item of items) {
        if (!item.types.includes("text/html"))
            continue;

        let reader = new FileReader;
        reader.addEventListener("load", loadEvent => {
            document.getElementById("html-output").innerHTML = reader.result;
        });
        reader.readAsText(await item.getType("text/html"));
        break;
    }
});
</script>

There are a couple of interesting things to note here:

  • Like writing data, reading data is also asynchronous; the processes of both fetching every ClipboardItem and extracting a Blob from a ClipboardItem return promises.
  • Type fidelities are preserved when reading data. This means the order in which types were written (either using system API on iOS and macOS, or the async clipboard API) is the same as the order in which they are exposed upon reading from the clipboard.

Security and Privacy

The async clipboard API is a powerful web API, capable of both writing arbitrary data to the clipboard, as well as reading from the system clipboard. As such, there are serious security ramifications when allowing pages to write data to the clipboard, and privacy ramifications when allowing pages to read from the clipboard. Clearly, untrusted web content shouldn’t be capable of extracting sensitive data — such as passwords or addresses — without explicit consent from the user. Other vulnerabilities are less obvious; for instance, consider a page that writes "text/html" to the pasteboard that contains malicious script. When pasted in another website, this could result in a cross-site scripting attack!

WebKit’s implementation of the async clipboard API mitigates these issues through several mechanisms.

  • The API is limited to secure contexts, which means that navigator.clipboard is not present for http:// websites.
  • The request to write to the clipboard must be triggered during a user gesture. A call to clipboard.write or clipboard.writeText outside the scope of a user gesture (such as "click" or "touch" event handlers) will result in the immediate rejection of the promise returned by the API call.
  • Both "text/html" and "image/png" data is sanitized before writing to the pasteboard. Markup is loaded in a separate document where JavaScript is disabled, and only visible content is then extracted from this page. Content such as <script> elements, comment nodes, display: none; elements and event handler attributes are all stripped away. For PNG images, the image data is first decoded into a platform image representation, before being re-encoded and sent to the platform pasteboard for writing. This ensures that a website cannot write corrupted or broken images to the pasteboard. If the image data cannot be decoded, the writing promise will reject. Additional information about WebKit’s sanitization mechanisms is available in the Clipboard API Improvements blog post.
  • Since users may not always be aware that sensitive content has been copied to the pasteboard, restrictions on the ability to read are more strict than the restrictions on the ability to write. If a page attempts to programmatically read from the pasteboard outside of a user gesture, the promise will immediately reject. If the user is explicitly triggering a paste during the gesture (for instance, using a keyboard shortcut on macOS such as ⌘V or pasting using the “Paste” action on the callout bar on iOS), WebKit will allow the page to programmatically read the contents of the clipboard. Programmatic clipboard access is also automatically granted in the case where the contents of the system clipboard were written by a page with the same security origin. If neither of the above are true, WebKit will show platform-specific UI which the user may interact with to proceed with a paste. On iOS, this takes the form of a callout bar with a single option to paste; on macOS, it is a context menu item. Tapping or clicking anywhere in the page (or performing any other actions, such as switching tabs or hiding Safari) will cause the promise to be rejected; the page is granted programmatic access to the clipboard only if the user manually chooses to paste by interacting with the platform-specific UI.
  • Similar to writing data, reading data from the system clipboard involves sanitization to prevent users from unknowingly exposing sensitive information. Image data read from the clipboard is stripped of EXIF data, which may contain details such as location information and names. Likewise, markup that is read from the clipboard is stripped of hidden content, such as comment nodes.

These policies ensure that the async clipboard API allows developers to deliver great experiences without the potential to be abused in a way that compromises security or privacy for users.

Future Work

As we continue to iterate on the async clipboard API, we’ll be adding support for custom pasteboard types, and will also consider support for additional MIME types, such as "image/jpeg" or "image/svg+xml". As always, please let us know if you encounter any bugs (or if you have ideas for future enhancements) by filing bugs on bugs.webkit.org.