clipboard, clipboard-polyfill, copy-to-clipboard, react-copy-to-clipboard, and vue-clipboard2 are JavaScript libraries that simplify copying text to the system clipboard across different browsers and environments. They abstract away the complexities of the native document.execCommand('copy') API (now deprecated) and the modern asynchronous navigator.clipboard.writeText() API, handling fallbacks, permissions, and browser inconsistencies. These packages differ significantly in scope: some are framework-agnostic utilities (clipboard, clipboard-polyfill, copy-to-clipboard), while others provide React or Vue-specific bindings (react-copy-to-clipboard, vue-clipboard2).
Copying text to the clipboard seems simple, but browser inconsistencies, security restrictions, and the deprecation of execCommand make it surprisingly tricky. The five packages under review tackle this problem differently — from DOM-centric helpers to modern promise-based utilities and framework wrappers. Let’s cut through the noise and see which tool fits your architecture.
Before diving in, note that vue-clipboard2 is officially deprecated. Its GitHub repository and npm page state: "This package is no longer maintained. Please use @soerenmartius/vue3-clipboard instead." Do not use it in new projects. We’ll still cover it briefly for legacy context, but treat it as a cautionary tale.
clipboard: Declarative DOM Bindingclipboard attaches copy behavior directly to HTML elements using data attributes. You never call a function — just add attributes like data-clipboard-text to a button, and the library handles the rest on click.
<!-- HTML setup -->
<button id="btn" data-clipboard-text="Hello World">Copy</button>
// JS initialization
import ClipboardJS from 'clipboard';
new ClipboardJS('#btn');
It listens for clicks, extracts text from attributes, and attempts to copy. Works only in response to user gestures (required by browser security).
clipboard-polyfill: Standards-Based Promise APIclipboard-polyfill provides a clean, modern interface aligned with the official navigator.clipboard API. It returns promises and handles fallbacks internally.
import { writeText } from 'clipboard-polyfill';
async function copyHandler() {
try {
await writeText('Hello World');
console.log('Copied!');
} catch (err) {
console.error('Failed to copy', err);
}
}
It automatically uses navigator.clipboard.writeText() when available and falls back to legacy methods (with proper sandboxing) in older browsers. Also works in non-DOM environments like Web Workers.
copy-to-clipboard: Minimalist Functioncopy-to-clipboard exports a single synchronous function that tries to copy text and returns a boolean success status.
import copy from 'copy-to-clipboard';
function handleClick() {
const success = copy('Hello World');
if (success) {
console.log('Copied!');
}
}
No promises, no callbacks — just fire-and-forget. Internally, it uses a temporary textarea element to perform the copy operation.
react-copy-to-clipboard: React Component WrapperThis package wraps copy-to-clipboard in a React component. You render it with text and onCopy props, and it clones its child with extra props.
import { CopyToClipboard } from 'react-copy-to-clipboard';
function App() {
return (
<CopyToClipboard text="Hello World" onCopy={() => console.log('Copied!')}>
<button>Copy</button>
</CopyToClipboard>
);
}
Under the hood, it calls copy-to-clipboard when the child element is clicked.
vue-clipboard2: Deprecated Vue DirectiveHistorically, this provided a Vue directive for copying:
// ❌ DO NOT USE — deprecated
import VueClipboard from 'vue-clipboard2';
Vue.use(VueClipboard);
<template>
<button v-clipboard:copy="'Hello World'">Copy</button>
</template>
But again — this package is unmaintained and should be avoided.
All these libraries must contend with two hard constraints:
navigator.clipboard outside of secure contexts.Here’s how they handle it:
clipboard: Only works on click events. Fails silently in unsupported contexts.clipboard-polyfill: Uses the standard permission API when possible. Throws clear errors when blocked, and includes a shouldShowPermissionPrompt() helper.copy-to-clipboard: Tries to copy immediately. Returns false if blocked, but gives no details why.react-copy-to-clipboard: Inherits copy-to-clipboard’s behavior — no error context.vue-clipboard2: Same as clipboard — gesture-bound with minimal error feedback.Your choice depends heavily on whether you prefer programmatic control (calling functions from JS) or declarative binding (attaching behavior via HTML/JSX).
clipboard-polyfill if you want modern promise handling and detailed error reporting.copy-to-clipboard if you just need a fire-and-forget boolean result.// clipboard-polyfill: async/await with error details
try {
await writeText(text);
} catch (err) {
if (err.name === 'NotAllowedError') {
showPermissionGuide();
}
}
// copy-to-clipboard: simple boolean
if (!copy(text)) {
alert('Copy failed — try manually');
}
clipboard for vanilla JS/jQuery apps with static buttons.react-copy-to-clipboard if you’re in React and like component composition.<!-- clipboard: pure HTML -->
<button data-clipboard-text="...">Copy</button>
{/* react-copy-to-clipboard: JSX */}
<CopyToClipboard text="..."><button>Copy</button></CopyToClipboard>
react-copy-to-clipboard feels natural but adds an extra dependency layer. Many teams skip it and call clipboard-polyfill directly inside event handlers for more control.// Common React pattern without wrapper
function CopyButton({ text }) {
const handleCopy = async () => {
try {
await writeText(text);
// update UI state, etc.
} catch (err) { /* handle */ }
};
return <button onClick={handleCopy}>Copy</button>;
}
vue-clipboard2 is dead, the best path is using clipboard-polyfill directly in methods:// Vue 3 Composition API example
import { writeText } from 'clipboard-polyfill';
export default {
methods: {
async copyText(text) {
try {
await writeText(text);
// handle success
} catch (err) {
// handle error
}
}
}
};
clipboard-polyfill is the safest bet for new projects. It’s actively maintained, follows web standards, and works everywhere from SPAs to Web Workers.| Package | API Style | Async Support | Error Details | Framework-Specific | Maintenance Status |
|---|---|---|---|---|---|
clipboard | Declarative DOM | ❌ | ❌ | None | ✅ Active |
clipboard-polyfill | Programmatic | ✅ Promise | ✅ | None | ✅ Active |
copy-to-clipboard | Programmatic | ❌ | ❌ (boolean) | None | ✅ Active |
react-copy-to-clipboard | React Component | ❌ | ❌ | React | ✅ Active |
vue-clipboard2 | Vue Directive | ❌ | ❌ | Vue 2 | ❌ Deprecated |
clipboard-polyfill. It’s the most future-proof, gives you detailed error handling, and aligns with web standards. Call it directly from your framework’s event handlers.copy-to-clipboard is fine.clipboard works well if you’re already using data attributes extensively.react-copy-to-clipboard unless you strongly prefer its component API — most teams gain more flexibility by using clipboard-polyfill directly.vue-clipboard2 entirely. Use clipboard-polyfill in methods or composables.Remember: no library can bypass browser security. Always design your UI to handle copy failures gracefully — show a fallback tooltip, highlight the text for manual selection, or guide users through permission prompts when needed.
Choose copy-to-clipboard if you want the simplest possible function-based API for programmatic copying without any DOM coupling. It’s a good fit for utility-heavy applications where you trigger copies from JavaScript logic rather than HTML attributes, and you prefer minimal abstraction over feature richness.
Choose react-copy-to-clipboard only if you’re working in a React codebase and prefer a component-based API that integrates naturally with JSX. Note that it wraps copy-to-clipboard under the hood, so it inherits its simplicity but adds React-specific ergonomics like render props or children-as-function patterns.
Choose clipboard if you need a DOM-focused utility that binds copy behavior directly to HTML elements via data attributes, and your app uses buttons or other interactive elements where declarative markup is preferred. It’s ideal for simple static sites or jQuery-style apps but requires manual DOM setup and doesn’t support programmatic copying outside of user-triggered events.
Choose clipboard-polyfill if you require a lightweight, modern, framework-agnostic solution that prioritizes standards compliance and works reliably in both browser and non-browser environments (like Web Workers). It supports both promise-based and callback APIs, handles permission prompts gracefully, and actively maintains compatibility with the latest web standards.
Avoid vue-clipboard2 in new projects — it is officially deprecated as of 2021 and unmaintained. The author recommends migrating to @soerenmartius/vue3-clipboard for Vue 3 or using a framework-agnostic alternative like clipboard-polyfill. Do not use it for production applications due to lack of updates and security patches.
Simple module exposing an async copy function that uses the Async Clipboard API (navigator.clipboard) in secure contexts (HTTPS / localhost), with automatic fallback to document.execCommand('copy') for non-secure contexts or older browsers.
import copy from 'copy-to-clipboard';
await copy('Text');
// Copy with options
await copy('Text', {
debug: true,
message: 'Press #{key} to copy',
});
// Copy as HTML (text/html + text/plain written simultaneously)
await copy('<b>Hello <i>world</i></b>', { format: 'text/html' });
// Custom plain-text fallback via onCopy
await copy('<b>Hello <i>world</i></b>', {
format: 'text/html',
onCopy: () => new ClipboardItem({
'text/html': new Blob(['<b>Hello <i>world</i></b>'], { type: 'text/html' }),
'text/plain': new Blob(['Hello world'], { type: 'text/plain' }),
}),
});
copy(text: string, options?: object): Promise<boolean> — copies text to clipboard. Returns true on success, false if all paths failed (no additional keystrokes were required from the user).
v4 breaking change:
copy()is now async and returnsPromise<boolean>. Useawait copy(...)to get the result.
| Value | Default | Notes |
|---|---|---|
options.debug | false | Boolean. Enable output to console. |
options.message | 'Copy to clipboard: #{key}, Enter' | String. Prompt message used when fallbackToPrompt is enabled. All occurrences of #{key} are replaced with ⌘+C on macOS or Ctrl+C otherwise. |
options.format | — | String. MIME type to copy as. Use 'text/html' to copy rich text; 'text/plain' to strip inherited styles when pasting into rich-text editors. When set alongside 'text/html', both text/html and text/plain are written simultaneously via ClipboardItem. |
options.onCopy | — | (clipboardData: ClipboardItem | DataTransfer) => ClipboardItem | void. Called before the write. On the async path, receives the constructed ClipboardItem and may return a new one to replace it (useful for custom MIME types or a different plain-text fallback). On the execCommand fallback path, receives the DataTransfer object; return value is ignored. |
options.fallbackToPrompt | false | Boolean. If true, shows a window.prompt() as a last resort when both navigator.clipboard and execCommand fail. Off by default in v4. |
navigator.clipboard.writeText(text) — used when the page is a secure context (HTTPS / localhost), navigator.clipboard is available, and neither options.format nor options.onCopy is set.navigator.clipboard.write([ClipboardItem]) — used in a secure context when options.format or options.onCopy is set. Builds a ClipboardItem with text/plain always present; adds the requested MIME type alongside it. onCopy may return a replacement ClipboardItem.execCommand('copy') fallback — used on non-HTTPS pages, when navigator.clipboard is unavailable, or when the async write throws. Uses a hidden <span> element. preventDefault is only called when options.format is set.window.prompt() fallback — opt-in via options.fallbackToPrompt: true.By default, copy(html, { format: 'text/html' }) puts the raw HTML string in the text/plain slot of the ClipboardItem. If you want apps that only accept plain text to receive readable content instead of markup, use onCopy to supply a stripped version:
function stripHtml(html) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
}
const html = '<b>Hello <i>world</i></b>';
await copy(html, {
format: 'text/html',
onCopy: () => new ClipboardItem({
'text/html': new Blob([html], { type: 'text/html' }),
'text/plain': new Blob([stripHtml(html)], { type: 'text/plain' }),
}),
});
// text/html → '<b>Hello <i>world</i></b>'
// text/plain → 'Hello world'
await copy('col1,col2\nval1,val2', {
format: 'text/csv',
});
// text/csv → 'col1,col2\nval1,val2'
// text/plain → 'col1,col2\nval1,val2' (always included as fallback)
| Browser | Minimum | Notes |
|---|---|---|
| Chrome | 76+ | Full ClipboardItem + write() support |
| Firefox | 127+ | Full ClipboardItem + write() landed in Firefox 127 (mid-2024) |
| Safari | 13.1+ | writeText() and write() available |
| Edge | 79+ | Chromium-based; same as Chrome |
execCommand fallback retains support for non-HTTPS contexts and any browser that reaches the catch path.
Note: The async clipboard write must occur within a user gesture (click, keydown, etc.). This library is designed to be called from event handlers, so this constraint is normally satisfied automatically.
npm i copy-to-clipboard
Available as CommonJS, ES module, and IIFE (for <script> tags):
// ESM
import copy from 'copy-to-clipboard';
// CommonJS
const copy = require('copy-to-clipboard');
<!-- IIFE via CDN — exposes window.copyToClipboard -->
<script src="https://unpkg.com/copy-to-clipboard/dist/index.global.js"></script>
Built-in declarations are included for both CommonJS and ESM consumers.
import copy from 'copy-to-clipboard';
import type { Options } from 'copy-to-clipboard';
const result: boolean = await copy('text');
End-to-end tests are powered by Nightwatch using native browser drivers.
npm i
npm test # Chrome (default)
npm run test:firefox
npm run test:safari
npm run test:edge
npm run test:all # all local browsers
Safari prerequisite: enable "Allow Remote Automation" in Safari's Develop menu. See Testing with WebDriver in Safari.
Chrome, Firefox, and Edge tests run automatically on every push to master and on pull requests via GitHub Actions (Ubuntu runner, headless).
Cross-browser tests (Chrome, Firefox, Safari, Edge) run on LambdaTest automatically on every version tag (v*) and can be triggered manually from the Actions tab. Requires LT_USERNAME and LT_ACCESS_KEY repository secrets.
npm run test:lt:chrome
npm run test:lt:firefox
npm run test:lt:safari
npm run test:lt:edge
npm run test:lt:all
copy() is now async — returns Promise<boolean> instead of boolean. Wrap call sites with await.window.clipboardData path removed.window.prompt() fallback is opt-in — set options.fallbackToPrompt: true to enable.dist/ — direct require('copy-to-clipboard/index.js') paths will break; use the package name only.