← Blog

Clipboard API Support on Mobile: iOS Safari and Android Chrome

A copy or paste feature that works flawlessly on desktop can fail quietly on mobile for reasons desktop developers never see: a gesture window that closes before the async call resolves, a permission prompt the WebView suppresses, a focus that never lands on the right element. This article surveys the actual behaviour of navigator.clipboard on iOS Safari and Android Chrome — plus the in-app WebView environments that behave differently from both — and the patterns that work across them.

The short summary

  • iOS Safari (17+): writeText and readText both work, but reads show a one-off prompt the first time per site and must be triggered inside a user gesture.
  • Android Chrome: essentially identical to desktop Chrome. Writes work without prompt; reads prompt on first use.
  • Android Firefox: writes work; reads can be unreliable in older versions.
  • In-app WebViews (iOS SFSafariViewController, Android Custom Tabs, Facebook / Instagram in-app browser): varies wildly. Often the API is present but throws, or the object itself is undefined.

The gesture window, tighter than on desktop

Mobile browsers enforce the user-gesture requirement more strictly. Desktop Chrome is forgiving about microtasks that happen a few ticks after the click; iOS Safari routinely rejects a writeText call that is not invoked synchronously in the click handler.

Anti-pattern that breaks on iOS:

button.onclick = async () => {
  const value = await fetchFromServer();
  await navigator.clipboard.writeText(value);
};

The network fetch breaks the gesture window, and the copy rejects. Two workable patterns:

  • Pre-fetch the value before the user clicks, and call writeText synchronously in the handler.
  • Use navigator.clipboard.write with a ClipboardItem backed by a Promise<Blob>. The promise resolves later, but the API accepts the deferred blob inside the user gesture.

Deferred blobs: the mobile-friendly write pattern

button.onclick = () => {
  const blob = fetchFromServer().then(text => new Blob([text], { type: 'text/plain' }));
  const item = new ClipboardItem({ 'text/plain': blob });
  navigator.clipboard.write([item]);
};

The call is synchronous from the browser's perspective, so the gesture check passes. The Clipboard API awaits the blob internally and lands the correct value on the clipboard once it resolves. Be aware that if the fetch takes too long (more than 5-10 seconds), iOS may cancel the operation silently.

Reads require extra ceremony

navigator.clipboard.readText() on iOS Safari shows a "Paste from Clipboard" prompt on first use per site. The prompt UI is small and native; some users miss it. If your read is triggered from a button that does not visually indicate a clipboard action, the user may not associate the prompt with the button.

Mitigations:

  • Label the trigger explicitly: "Read clipboard" or "Paste" rather than an ambiguous icon.
  • After the first grant, iOS caches the permission for that site for a while. Do not assume every call will prompt.
  • Handle the rejection cleanly — if the user dismisses the prompt, readText rejects with NotAllowedError and the next call starts over.

In-app WebViews: expect the worst

When users visit your site through the Facebook or Instagram in-app browser, they are running inside a WebView with a different permission model. Common failures:

  • navigator.clipboard is present but every call rejects.
  • Writes succeed but the value does not land on the OS clipboard — it lands in an ephemeral app-local clipboard.
  • Reads always reject with NotAllowedError.

Detect these environments where possible. navigator.userAgent reveals the host app (look for FBAN, FBAV, Instagram, Line, Twitter). For detected in-app browsers, default the UI to the fallback textarea approach and consider showing a "Open in default browser" hint for features that need the clipboard to work properly.

Focus behaviour differs

On mobile, tapping a button does not always give the page focus the way a desktop click does. A readText call from a handler on a page that lost focus to a system keyboard or a previous navigation can reject with NotAllowedError even inside a user gesture. Call window.focus() as a defensive measure — it is a no-op when focus is already correct and sometimes recovers an otherwise-failing call.

Image clipboard support is thin on mobile

Reading images via navigator.clipboard.read() works on Android Chrome but is unreliable on iOS Safari. Writing images via clipboard.write with an image/png ClipboardItem works on Android but often fails silently on iOS. For features that need cross-platform image clipboard, an in-app fallback (rendering the image for the user to long-press and save) is usually the right call.

The paste event still works

When navigator.clipboard fails, the traditional paste event often still fires. An input with autocomplete="off" and a paste handler is a reliable way to accept clipboard content on mobile:

<input type="text" placeholder="Paste here" onpaste="handlePaste(event)">

Users tap the field, long-press, select Paste from the iOS/Android menu, and your handler fires with full clipboardData access. This is the most reliable cross-device paste path — it sidesteps the permissions model entirely.

Testing on real devices

Emulators lie. Xcode's iOS simulator reports navigator.clipboard as available but runs in a different permissions model than a real iPhone. Chrome DevTools mobile emulation reports desktop Chrome's clipboard behaviour. Test on a real device for any clipboard feature that ships to mobile users.

Clipboard Inspector works on mobile and gives you a live view of what the browser actually delivers on each paste. It is the fastest way to see whether a mystery mobile paste bug is in your handler or in the upstream clipboard state. For the permission states themselves, see the Permissions API guide.