跪拜 Guibai
← Back to the summary

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

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

  1. Same for imagescopyImageFromUrl should also create the ClipboardItem immediately within the user gesture context.
  2. Fallback – Always provide execCommand('copy') or the copy-to-clipboard library as a fallback.
  3. Error handling – The Clipboard API can fail due to permissions, HTTPS, etc., so robust error handling is needed.

References

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

szp2005 1 likes

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.

Wilson王艺谋

Productive exchange [handshake]