← Blog

Writing Custom application/* MIME Types to the Clipboard

When you copy an object inside a design tool like Figma and paste it into another Figma tab, all the nuance — vector paths, layer hierarchy, fills with their exact colour profiles — arrives intact. That does not happen through text/plain or even text/html. It happens through a custom application/* MIME type that the app writes to the clipboard on copy and reads on paste. This article walks through how to do the same thing in your own web app: losslessly copy complex data between tabs using custom MIME types.

What a custom MIME type is

Any string in MIME-type format can be used as a clipboard key. Conventionally, application/* types are for binary or app-specific formats, and the vnd. prefix marks a vendor-specific type. Examples from the wild:

  • application/vnd.figma — Figma's internal object format.
  • application/x-vnd.google-docs-document-slice-clip+wrapped — Google Docs' own serialisation.
  • application/x-officedraw — Microsoft Office's drawing format.

For your own app, pick a name that identifies your company and the data kind: application/vnd.myapp.widget+json works well.

Writing a custom type

The async Clipboard API's ClipboardItem constructor accepts any MIME key:

const widget = { id: 'abc', color: '#ff0000', children: [] };
const item = new ClipboardItem({
  'application/vnd.myapp.widget+json': new Blob([JSON.stringify(widget)], { type: 'application/vnd.myapp.widget+json' }),
  'text/plain': new Blob([widget.id], { type: 'text/plain' }),
});
await navigator.clipboard.write([item]);

Always pair a custom type with a text/plain fallback. A user who pastes your widget into a text editor expects to see something — at minimum a readable label so the paste was not silent.

Writing binary data

If your custom payload is binary (a protobuf-encoded blob, a binary MessagePack structure, or a WASM-produced buffer), the same pattern works. ClipboardItem accepts any Blob, including one constructed from ArrayBuffer:

const buf = serializeWidget(widget); // returns ArrayBuffer
const item = new ClipboardItem({
  'application/vnd.myapp.widget': new Blob([buf], { type: 'application/vnd.myapp.widget' }),
  'text/plain': new Blob([describe(widget)], { type: 'text/plain' }),
});
await navigator.clipboard.write([item]);

Reading a custom type

On the paste side, both the async API and the traditional paste event expose custom types, but the async API is more reliable. Chrome, Firefox, and Safari ship the async API with full custom-type support. The paste event often strips them as a security measure, particularly for cross-origin pastes.

const items = await navigator.clipboard.read();
for (const item of items) {
  if (item.types.includes('application/vnd.myapp.widget+json')) {
    const blob = await item.getType('application/vnd.myapp.widget+json');
    const widget = JSON.parse(await blob.text());
    return insertWidget(widget);
  }
}

Fall back to text/plain or text/html if the custom type is not present. Users pasting from other apps will not produce your custom format — support both paths.

Size limits

Browsers impose implementation-specific limits on clipboard payload size. Chrome's limit is effectively "as much memory as you have" for the async API but a much smaller limit for the paste event. Safari historically caps custom types at a few MB. For data under 1 MB, you will not hit limits in practice. For larger payloads, split into chunks and reassemble or — better — put a reference (an ID or URL) on the clipboard and fetch the actual bytes from your server when pasted.

The reference pattern

For very large payloads, or for data that makes no sense outside your app, put a stable identifier on the clipboard and resolve it on paste:

const item = new ClipboardItem({
  'application/vnd.myapp.widget-ref+json': new Blob([JSON.stringify({ id: widget.id, origin: location.origin })], { type: 'application/vnd.myapp.widget-ref+json' }),
  'text/plain': new Blob([widget.name], { type: 'text/plain' }),
});

On paste, the receiving tab reads the ID, fetches the full object from your server, and inserts it locally. This works across devices (if the user is signed in on both), supports arbitrary size, and scales to data that could never fit in a clipboard payload.

Security considerations

A custom MIME type is no more or less secure than any other clipboard content — it is data the user voluntarily copied and voluntarily pasted. But the recipient side of a custom-type paste is a new attack surface. Never eval pasted content, never construct DOM from pasted HTML without sanitisation, and always validate the pasted JSON against your expected schema.

If your custom type carries user references (user IDs, document IDs), verify authorisation server-side when the pasted data is used — a user who pastes into their own tab could be pasting a reference they should not have.

Across devices

Modern operating systems sync the clipboard across devices (Apple's Universal Clipboard, Microsoft's Cloud Clipboard). These work for text and images but typically not for custom application/* types — custom types live only on the device that originated the copy. If cross-device paste matters, the reference pattern above is the only reliable path: the ID makes it across, and the resolve step works device-agnostic.

Multiple copies, one clipboard

The write method accepts an array of ClipboardItems. In practice, every browser supports only a single item at a time: the second one is ignored. Do not design flows that depend on multi-item clipboards. A single item with multiple MIME keys is the right pattern.

Debugging custom types

When a paste of your custom type is not arriving correctly, paste into Clipboard Inspector. The inspector enumerates every MIME type and shows the size and first bytes of each. If your custom type is missing from the inspector's list, the write path is broken. If it is present but your app's paste handler does not see it, the receive path is wrong. For the underlying behaviour of the paste event versus the async API, see Clipboard API vs Paste Event.