copy-to-clipboard: a small async copy function that picks the right clipboard path for you
Copy stuff into clipboard from your browser using JS
At a glance
- What is it?
- copy-to-clipboard is a small JavaScript module whose whole job is putting text on the clipboard, with four paths tried in order depending on whether the page is a secure context, and a v4 release that turned copy() into an async function returning a boolean. It is MIT licensed, ships TypeScript declarations, and its browser support table explains what you get where.
- Who is it for?
- copy-to-clipboard fits a browser application that needs a copy button, that runs on HTTPS so the modern clipboard path is available, and that wants one dependency instead of a paste-and-clipboard polyfill bundle. It does not fit an Electron application, since the project tells you to use the Electron clipboard API instead, and it does not fit a case where you need a plain string result synchronously.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 79 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Four copy paths, chosen by context rather than by you
The library is described as a simple module exposing an async copy function that uses the Async Clipboard API, navigator.clipboard, in secure contexts, meaning HTTPS or localhost, with automatic fallback to document.execCommand('copy') for non-secure contexts or older browsers.
The path selection is documented as an ordered list, and reading it tells you most of what you need to know about the library.
The first path is navigator.clipboard.writeText(text), used when the page is a secure context, navigator.clipboard is available, and neither options.format nor options.onCopy is set. This is the plain case, and it is the one that needs no special handling from you.
The second path is navigator.clipboard.write with a ClipboardItem, used in a secure context when options.format or options.onCopy is set. It builds a ClipboardItem with text/plain always present and adds the requested MIME type alongside it, and onCopy may return a replacement ClipboardItem.
The third path is the execCommand('copy') fallback, used on non-HTTPS pages, when navigator.clipboard is unavailable, or when the async write throws. It works by using a hidden span element, and preventDefault is only called when options.format is set.
The fourth path is a window.prompt() fallback, opt-in through options.fallbackToPrompt set to true.
The order is the design. A library that asked you to pick a strategy would leak the browser's capability matrix into every call site. Here you describe what you want to copy and, if you need something unusual, you hook one option.
The v4 change that breaks every existing call site
The API is copy(text: string, options?: object): Promise<boolean>, which copies text to clipboard and returns true on success, or false if all paths failed, with the note that no additional keystrokes were required from the user.
The v4 breaking change is stated plainly: copy() is now async and returns Promise<boolean>, and you should use await copy(...) to get the result.
That is a large change for a small library, and it is worth being clear about why it is not gratuitous. The old style of copy function returned synchronously and reported success by throwing or by a boolean side effect, and it could not tell you which path had been taken. Returning a promise that resolves to a boolean tells you whether the text actually reached the clipboard, and that is the difference between a copy button that gives feedback and one that lies.
The false return also has a precise meaning. It is returned if all paths failed, and the parenthetical about no additional keystrokes being required from the user is the key. If the fallbackToPrompt path was taken and the user typed the text into the prompt, that is not a failure, because the user did copy the content, they just did it themselves.
The options table makes the rest of the contract explicit. debug is a Boolean defaulting to false that enables output to console. message is a String defaulting to 'Copy to clipboard: #{key}, Enter' used when fallbackToPrompt is enabled, with all occurrences of #{key} replaced with ⌘+C on macOS or Ctrl+C otherwise. format is the MIME type to copy as, and the note explains that 'text/html' copies rich text while 'text/plain' strips inherited styles when pasting into rich text editors, and that when set alongside 'text/html' both are written simultaneously via ClipboardItem. onCopy is a function called before the write, receiving the constructed ClipboardItem on the async path and possibly returning a new one, and receiving the DataTransfer on the execCommand path where the return value is ignored. fallbackToPrompt is a Boolean defaulting to false, off by default in v4.
The one design point that will surprise people is that the default prompt message contains #{key} as a placeholder rather than a literal chord, because the same option has to render differently on macOS and everywhere else.
Rich text copying, and the plain text slot that needs stripping
The recipes section covers a case that is easy to get wrong, and the reason the library exposes onCopy at all is this case.
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, you 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' }),
}),
});The result is that text/html carries the markup and text/plain carries the readable text.
The same mechanism handles a custom MIME type, which is the more generally useful half of the feature.
await copy('col1,col2\nval1,val2', {
format: 'text/csv',
});Here text/csv carries the value and text/plain is always included as the fallback, which is the same invariant as the HTML case. The rule the library holds to is that text/plain is always present, and whatever else you ask for is added alongside it.
The reason this needs a hook rather than a flag is that the plain text variant is a different string, not a transformation of the same one, and the library has no opinion about how you should strip HTML. It could have implemented a stripping routine, and then every application with a slightly different notion of what readable text means would have to override it anyway. Making the caller supply the fallback is the more honest design.
Browser support, and the one constraint that is not about browsers
The support table is short and each row carries a reason.
| 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 |
The Firefox row is the one to notice, because the date is recent. Full ClipboardItem and write support landed in Firefox 127 in mid-2024, so a browser support claim that predates that is wrong for the rich text path.
The table also has a footnote: the execCommand fallback retains support for non-HTTPS contexts and any browser that reaches the catch path. So the old path is not dead, it is the safety net, and it works in exactly the cases where the modern path is unavailable.
The constraint that is not about browsers is stated in a note at the end. The async clipboard write must occur within a user gesture, a click, a keydown, and so on. The library is designed to be called from event handlers, so this constraint is normally satisfied automatically.
That is the whole failure mode of the modern clipboard API in one sentence. There is no permission prompt, no dialog, and no way to copy in a timer callback or a promise chain that is not downstream of a user action. A page that tries to copy on a timer will find the write rejected, and if the page is not in a secure context the fallback path requires selecting a hidden span, which the browser will not do without a gesture either. This is the reason the fourth path, the prompt, exists.
Installing it, and the three module formats
Installation is a single npm command.
npm i copy-to-clipboardThe package is available as CommonJS, ES module and IIFE for script tags, and the ESM and CommonJS forms are shown side by side.
// ESM
import copy from 'copy-to-clipboard';
// CommonJS
const copy = require('copy-to-clipboard');The IIFE build is loaded from a CDN in the example, and it exposes a global.
<script src="https://unpkg.com/copy-to-clipboard/dist/index.global.js"></script>TypeScript declarations are included for both CommonJS and ESM consumers, and the types are exported alongside the default function.
import copy from 'copy-to-clipboard';
import type { Options } from 'copy-to-clipboard';
const result: boolean = await copy('text');The package manifest backs the three format claim. The main field points at dist/index.cjs, the module field at dist/index.mjs, the types field at dist/index.d.ts, and the exports map has separate import and require conditions each with its own types entry, one pointing at dist/index.d.mts for the ESM side. The type is commonjs, which is what lets a single package ship a real ESM build alongside it, and sideEffects is false, which lets a bundler drop the module entirely if it can prove the import is unused.
There is one important note near the top of the README. If you are building using Electron, use their API instead. That is not a preference, it is a correctness point, because the Electron clipboard API is synchronous and available in the main process, so the whole fallback chain this library exists to manage is unnecessary there.
The test matrix, and what it tells you about the project
The end to end tests are powered by Nightwatch using native browser drivers, and the scripts in the package manifest are worth reading because they describe the support policy in executable form.
npm i
npm test # Chrome (default)
npm run test:firefox
npm run test:safari
npm run test:edge
npm run test:all # all local browsersThere is a Safari prerequisite, and it is the kind of detail that saves an afternoon: enable Allow Remote Automation in Safari's Develop menu. The documentation links to Apple's page on testing with WebDriver in Safari.
The scripts also include a parallel set for LambdaTest, one per browser plus a combined run, which tells you the browser matrix is exercised on hosted infrastructure as well as locally.
Continuous integration runs the Chrome, Firefox and Edge tests automatically on every push to master and on pull requests.
The build and test wiring in the manifest is careful in a way that suggests the maintainer cares about the package's shape. There is a pretest hook that runs the build first, so the tests never run against a stale bundle. The build itself is tsup plus a copy of dist/index.global.js over example/index.js, so the CDN example always matches the published build. There is a typecheck script running tsc with noEmit, a check script running publint, which validates the package metadata against the exports map, and a set of changelogen scripts for patch, minor and major releases, each with the no-bump variant for regenerating the changelog without cutting a version.
So a v4 release in this repository is a single scripted command, and the version in the manifest, 4.0.2, is the single source of truth for what npm holds.
Editorial conclusion
copy-to-clipboard fits a browser application that needs a copy button, that runs on HTTPS so the modern clipboard path is available, and that wants one dependency instead of a paste-and-clipboard polyfill bundle. It does not fit an Electron application, since the project tells you to use the Electron clipboard API instead, and it does not fit a case where you need a plain string result synchronously. Check three things before you commit. Check whether the copy happens inside a user gesture, because the async clipboard write must occur within a click or keydown and the library is designed to be called from event handlers so this is normally satisfied automatically. Check whether your page is served over HTTPS or on localhost, since outside a secure context the library drops to the execCommand path, which is deprecated and works by selecting a hidden span. And check whether you need rich text, because the format option and the onCopy hook are what make it more than a one liner, and they require a secure context. The last push was on 2026-08-05, the newest release listed is v4.0.2 from 2026-04-24, and the license is MIT.
Frequently asked questions
How do I install and use copy-to-clipboard?
Run npm i copy-to-clipboard, then import copy from 'copy-to-clipboard' for ESM or require it for CommonJS. The copy function is async, so you await copy('Text') and get a boolean back telling you whether the clipboard write succeeded.
What changed in version 4 of copy-to-clipboard?
copy() is now async and returns Promise<boolean>. The breaking change notice says to use await copy(...) to get the result, and the prompt fallback is off by default in v4.
How do I copy rich text with copy-to-clipboard?
Pass the format option, for example format: 'text/html'. When set alongside text/html, both text/html and text/plain are written simultaneously via ClipboardItem, and you can use the onCopy hook to supply your own plain text fallback if the default raw markup is not what you want.
Which browsers does copy-to-clipboard support?
Chrome 76 and newer, Firefox 127 and newer for full ClipboardItem and write support, Safari 13.1 and newer, and Edge 79 and newer. The execCommand fallback retains support for non-HTTPS contexts and browsers that reach the catch path.
Does copy-to-clipboard work in Electron?
The README says that if you are building using Electron you should use their API instead, since the Electron clipboard API is available in the main process and does not need the fallback chain this library manages.
What license is copy-to-clipboard released under?
MIT, as declared in the package manifest with a LICENSE file at the root of the repository. The newest listed release is v4.0.2 from 2026-04-24, and the last push was on 2026-08-05.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sudodoki-copy-to-clipboard)Was this useful?