Resolving Mobile Clipboard Copy Issues with navigator.clipboard and a document.execCommand Fallback
This article explains why a copy button failed on mobile browsers, compares the modern Clipboard API with the deprecated document.execCommand method, and provides a robust fallback implementation in a custom React hook that works across browsers and secure contexts.
When I first joined the team, a bug surfaced where clicking the copy button did not copy text on mobile devices, prompting the product manager to question the quality of the feature and its acceptance testing.
Investigation revealed that the copy logic relied on navigator.clipboard.writeText() , which worked locally but raised compatibility concerns on mobile browsers, leading me to research support for this API.
According to MDN, the Clipboard API is exposed via the Navigator interface as a read‑only clipboard property that returns a Clipboard object. The API provides asynchronous read/write methods that require explicit user permission through the Permissions API using "clipboard-read" and/or "clipboard-write".
For browsers that do not support the Clipboard API, the traditional document.execCommand() method can be used as a fallback. It supports commands such as copy , cut , and paste , though it is considered deprecated.
Comparatively, the Clipboard API returns a Promise , avoiding UI blocking when handling large data, but it is limited to secure contexts (HTTPS or localhost) and newer browser versions. The execCommand approach has broader compatibility but requires more manual handling and is slated for removal in future browser releases.
To address the issue, I updated the custom hook to first attempt the modern Clipboard API and, if unavailable or insecure, fall back to a document.execCommand('copy') implementation. The full revised code is shown below:
import Toast from "~@/components/Toast";
export const useCopy = () => {
const copy = async (text: string, toast?: string) => {
const fallbackCopyTextToClipboard = (text: string, toast?: string) => {
let textArea = document.createElement("textarea");
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = "-200";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
// textArea.focus();
textArea.select();
let msg;
try {
let successful = document.execCommand("copy");
msg = successful ? toast ? toast : "复制成功" : "复制失败";
} catch (err) {
msg = "复制失败";
}
Toast.dispatch({
content: msg,
});
document.body.removeChild(textArea);
};
const copyTextToClipboard = (text: string, toast?: string) => {
if (!navigator.clipboard || !window.isSecureContext) {
fallbackCopyTextToClipboard(text, toast);
return;
}
navigator.clipboard
.writeText(text)
.then(() => {
Toast.dispatch({
content: toast ? toast : "复制成功",
});
})
.catch(() => {
fallbackCopyTextToClipboard(text, toast);
});
};
copyTextToClipboard(text, toast);
};
return copy;
};After deploying this change, the copy functionality has been stable for nearly a year without further incidents.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.