You click a copy button during local development and see NotAllowedError in the console. Switch to iPhone for testing and navigator.clipboard is flat-out undefined. Both situations share the same root cause: navigator.clipboard requires a Secure Context β HTTPS or localhost β but iOS Safari doesn’t fully honor localhost either.
The fix is straightforward, but doing it cleanly requires one thing: navigator.clipboard.writeText() returns a Promise, while clipboard.js’s ClipboardJS.copy() is synchronous. To make both paths transparently interchangeable, the fallback needs to be wrapped as a Promise too.
Why a Unified Promise Interface Matters
The signature of navigator.clipboard.writeText():
| |
Callers use await or .then() β clear and consistent. If the fallback is synchronous, callers have to detect which path to take themselves, scattering that logic everywhere.
Wrapping everything into the same Promise<void> means callers always see one function. Which path runs underneath is none of their business.
The Implementation
| |
Two paths only:
navigator.clipboardexists β return it directly, it’s already a Promise- Doesn’t exist β run
ClipboardJS.copy(), wrap the synchronous result innew Promise,resolve()on success,reject()on error
Integrating with Any Framework
With this function, how you use it is entirely up to the caller.
Vanilla JS
| |
Alpine.js
| |
Vue
| |
Custom Event (for Livewire or cross-component use)
Trigger via DOM event β no direct import needed:
| |
Livewire dispatch:
| |
The Role of clipboard.js
clipboard.js uses document.execCommand('copy') under the hood β an older API that doesn’t require a Secure Context and works on both HTTP and iOS. It handles cross-browser edge cases, saving you from manually wiring up a textarea.
execCommand is marked deprecated, but all major browsers still support it and it isn’t going away anytime soon. It remains the most reliable fallback available.
Installation
| |
TypeScript types are bundled β no separate @types/clipboard needed.
