How to write to the clipboard when using async in Safari
When using APIs like navigator.clipboard.write
or navigator.clipboard.writeText
, they require transient activation - which is to say they need to be triggered by user interaction.
This stops a site from hijacking your clipboard by simply visiting it but it can sometimes be a little too strict, in Safari’s case.
A relatively simple use-case we use in Cloudflare’s documentation is a Copy Page as Markdown powered by our index.md
pages.
When the button is pressed, we want to fetch index.md
(relative to the current path) & write the body to the clipboard. In Chrome & Firefox, this is as simple as…
button.addEventListener("click", async () => { const response = await fetch("index.md"); const text = await response.text(); await navigator.clipboard.writeText(text);});
In Safari, this won’t work since the use of an async method between the user interaction & the clipboard write is not allowed. You’ll get an error like this:
NotAllowedError: The request is not allowed by the user agent or the platform in current context, possibly because the denied permission.
To work around this, you’ll need to use a ClipboardItem
which can be given a promise (in this case, our fetch
with a chained .then
):
button.addEventListener("click", async () => { const response = await fetch("index.md"); const text = await response.text(); await navigator.clipboard.write([ new ClipboardItem({ ["text/plain"]: fetch(markdownUrl) .then((r) => r.text()) .then((t) => new Blob([t], { type: "text/plain" })), }), ]);});
This might look a little weird with us creating our own Blob
- why don’t we just return a string
since the spec supports that? Well, I found this didn’t work in Chrome (it would silently fail and just write an empty string to the clipboard).
You also can’t just return r.blob()
in our case as the MIME type for these endpoints is text/markdown
& that is not allowed.