I recently worked on a small feature to programmatically copy a piece of text containing a hyperlink. I'm talking about strings that look something like this:
Hypertext is really cool!
You'll notice that the word "Hypertext" contains a hyperlink to the Wikipedia page of the same name. If you highlight, copy, and paste the sentence, you'll notice that the hyperlink is preserved if you paste it into, e.g., Gmail or another app that supports rich text, but is shown without the hyperlink if you paste it into, say, VS Code.
Now let's consider a small twist: instead of manually highlighting and copying the text, I want to build a button that copies the hypertext to the clipboard when clicked. To implement that, we need to turn to the Clipboard API, which is widely available in all major browsers these days. Here's a simple example of how to use it to copy an HTML link in rich text:
const link = "https://en.wikipedia.org/wiki/Special:Random";
const clipboardItem = new ClipboardItem({
'text/html': new Blob(
[`<a href="${link}">Here\'s the link for item #43092</a>`],
{ type: 'text/html' }
),
});
await window.navigator.clipboard.write([clipboardItem]);
This kind of works: if you populate your clipboard with this method and then paste it into Gmail, you'll get the hypertext you'd expect. However, if you try to paste it in VS Code, nothing happens. We can fix this by adding a plain text fallback to the clipboard. It might not be immediately obvious how to achieve this just by looking at the MDN docs for ClipboardItem
, but the solution is to add another property with the MIME type text/plain
to the ClipboardItem
constructor's data
argument:
const link = "https://en.wikipedia.org/wiki/Special:Random" ;
const plainText = "Here's the link for item #43092";
const clipboardItem = new ClipboardItem({
'text/html': new Blob(
[`<a href="${link}">${plainText}</a>`],
{ type: 'text/html' }
),
'text/plain': `${plainText}: ${link}`, // Add the plain text fallback
});
await window.navigator.clipboard.write([clipboardItem]);
This almost works. Using this method on Safari and Firefox, things work as you'd expect: if you paste to Gmail you get the hypertext version, and pasting to VS Code yields the plain text version. However, Chrome does not play ball, and trying to copy the text using this code throws the following error:
NotAllowedError: Failed to execute 'write' on 'Clipboard': DOMString is not supported in ClipboardItem
It's a slightly vague error, but really it just means that Chrome wants you to make that plain text part into a Blob
. Let's fix it:
const link = "https://en.wikipedia.org/wiki/Special:Random" ;
const plainText = "Here's the link for item #43092";
const clipboardItem = new ClipboardItem({
'text/html': new Blob(
[`<a href="${link}">${plainText}</a>`],
{ type: 'text/html' }
),
'text/plain': new Blob(
[`${plainText}: ${link}`],
{ type: 'text/plain' }
),
});
await window.navigator.clipboard.write([clipboardItem]);
And ta-da! Now it works great on Chrome, Firefox, and Safari!
Afterword
As I was implementing this small feature, I ran into some interesting tidbits that might be good to know if you're building similar functionality:
Make Sure Your URL Includes The Protocol
If the href
you provide to the anchor element doesn't have a proper protocol prefix (like https://
) you might be in for a bad time. For example, pasting to Slack with a link like this yields the hypertext's plain text content only, and ignores the hyperlink. On Chrome, a link like "wikipedia.org" gets the current host prefixed to it; if my current host is http://localhost:5001
, then the href
would turn out as http://localhost:5001/wikipedia.org
. You'll definitely want to make sure your URLs are correctly formatted!
Different Browsers Populate The Clipboard Differently
The browser you use to copy the hypertext affects details beyond just the potential domain prefix. As I was debugging the issues mentioned above, I studied the raw clipboard contents with the following snippet on macOS:
osascript -e 'the clipboard as record' | cat
Note that the HTML comes out encoded in base16, but it's pretty easy to decode with a CLI or online tool of your choice.
Here's how the raw clipboard content looks like when the hypertext is copied on different browsers on my macOS (formatted for a more pleasant reading experience):
Chrome 132
<meta charset="utf-8" />
<html>
<head></head>
<body>
<a href="https://en.wikipedia.org/wiki/Special:Random">
Here\'s a link for item #43092
</a>
</body>
</html>
Safari 18.3
<a
href="https://en.wikipedia.org/wiki/Special:Random"
style="
font-style: normal;
font-variant-caps: normal;
font-weight: 400;
letter-spacing: normal;
orphans: auto;
text-align: start;
text-indent: 0px;
text-transform: none;
white-space: normal;
widows: auto;
word-spacing: 0px;
-webkit-text-stroke-width: 0px;
"
>
Here\'s a link for item #43092
</a>
The orphans
and widows
CSS attributes are somewhat exotic in your typical run-of-the-mill web developer work, so it was cool to see them added here. If you're wondering what they're used for, you can find a neat explanation here. The main takeaway is that these are relevant in print layouts.
Firefox 136
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
<html>
<head></head>
<body>
<a href="https://en.wikipedia.org/wiki/Special:Random">
Here\'s a link for item #43092
</a>
</body>
</html>
</body>
</html>