iOS Safari’s Clipboard Permission Dialog Has a One-Line Fix
Problem
When using navigator.clipboard.write() to copy text in iOS Safari, a system permission dialog pops up asking for user confirmation. This does not happen in other browsers (Chrome, Firefox, desktop Safari).
Root Cause
iOS Safari imposes strict security restrictions on the Clipboard API: clipboard.write() must be called within the synchronous execution context of a user gesture (click).
Incorrect Example
const handleCopy = async () => {
const link = await fetchLink(); // ❌ After the async operation, the user gesture context is lost
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': new Blob([link], { type: 'text/plain' }),
}),
]);
};
In the code above, after await fetchLink() completes, the synchronous context of the user click is already lost. Calling clipboard.write() at this point triggers the permission dialog.
Correct Example
const handleCopy = async () => {
// ✅ Create the ClipboardItem immediately within the user gesture context, passing in a Promise
const clipboardItem = new ClipboardItem({
'text/plain': fetchLink().then(link => new Blob([link], { type: 'text/plain' })),
});
await navigator.clipboard.write([clipboardItem]);
};
The ClipboardItem constructor accepts Promise<Blob> as a value. Creating the ClipboardItem immediately within the user gesture context and passing in a Promise lets the browser "reserve" the clipboard write operation. It waits for the Promise to resolve before writing the data, thereby avoiding the permission dialog.
Technical Points
| Method | Purpose | iOS Safari Permission Requirement |
|---|---|---|
clipboard.writeText() |
Copy plain text | Relatively lenient, recommended for synchronous text |
clipboard.write() |
Copy arbitrary data | Strict, must be within a user gesture context |
Value Types Supported by ClipboardItem
BlobPromise<Blob>string(some browsers)Promise<string>(some browsers)
Wrapper Recommendation
type TextInput = string | Promise<string> | (() => string | Promise<string>);
async function copyToClipboard(textInput: TextInput): Promise<boolean> {
// Normalize to Promise
const textPromise =
typeof textInput === 'function'
? Promise.resolve(textInput())
: Promise.resolve(textInput);
if (navigator.clipboard?.write && typeof ClipboardItem !== 'undefined') {
try {
// Key: create ClipboardItem immediately, passing in a Promise
const clipboardItem = new ClipboardItem({
'text/plain': textPromise.then(text => new Blob([text], { type: 'text/plain' })),
});
await navigator.clipboard.write([clipboardItem]);
return true;
} catch {
// Fallback
}
}
// Fallback to execCommand or a third-party library
const text = await textPromise;
return fallbackCopy(text);
}
Usage
// Synchronous text
copyToClipboard('static text');
// Async text fetch – pass a Promise
copyToClipboard(fetchLink());
// Async text fetch – pass a function (recommended)
copyToClipboard(() => fetchLink());
Notes
- Same for images –
copyImageFromUrlshould also create theClipboardItemimmediately within the user gesture context. - Fallback – Always provide
execCommand('copy')or thecopy-to-clipboardlibrary as a fallback. - Error handling – The Clipboard API can fail due to permissions, HTTPS, etc., so robust error handling is needed.
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
There's also a pitfall on the read side: clipboard.read() also triggers the permission prompt. Switching to listening for the paste event and picking image/* from clipboardData.items to get a File bypasses that API. On the write side, we happened to step on the wrong example you mentioned — will fix it later.
Productive exchange [handshake]