@wcstack/clipboard
v1.13.0
Published
Declarative clipboard component for Web Components. Framework-agnostic Clipboard API primitive via wc-bindable-protocol.
Maintainers
Readme
@wcstack/clipboard
@wcstack/clipboard is a headless clipboard component for the wcstack ecosystem.
It is not a visual UI widget.
It is an async primitive node that turns clipboard access into reactive state — the same way @wcstack/fetch turns a network request into reactive state and @wcstack/geolocation turns the device's location into reactive state.
Unlike geolocation (a read-only sensor), the clipboard is bidirectional, which makes <wcs-clipboard> the showcase for both directions of the wc-bindable token protocol:
- write (
state → element) via the command-token protocol —command.writeText: $command.copy - read (
element → state) via command results, plus a monitor mode that republishes the user'scopy/cut/pastevia the event-token protocol —eventToken.pasted: clipboardPasted
With @wcstack/state, <wcs-clipboard> can be bound directly through path contracts:
- input surface:
monitor - command surface:
writeText,write,readText,read,startMonitor,stopMonitor - output state surface:
text,items,loading,error,readPermission,writePermission,monitoring,copied,cut,pasted
This means clipboard work can be expressed declaratively in HTML, without writing navigator.clipboard.writeText(), readText(), read(), event listeners, or teardown glue in your UI layer.
@wcstack/clipboard follows the CSBC (Core / Shell / Binding Contract) architecture:
- Core (
ClipboardCore) handles read/write, richClipboardItemnormalization, error handling, monitor subscriptions, and live permission tracking - Shell (
<wcs-clipboard>) connects that state to DOM attributes, lifecycle, and declarative commands - Binding Contract (
static wcBindable) declares observableproperties, writableinputs, and callablecommands
Why this exists
The Clipboard API is, like fetch, an asynchronous source of values — but it is bidirectional (read and write) and gated by two separate permissions (clipboard-read / clipboard-write). Imperatively it requires gesture-bound calls, permission queries, event wiring, and cleanup on disconnect.
@wcstack/clipboard moves that logic into a reusable component and exposes the result as bindable state. A copy or a paste becomes a state transition, not imperative callback wiring.
Secure context + user gesture required. The Clipboard API only works in a secure context (HTTPS, or
localhost). Writes (writeText/write) require transient activation — call them from a click handler or a command-token wired to a user action. Reads (readText/read) require focus and read permission. Whennavigator.clipboardis absent (non-secure context or unsupported browser), commands surface aNotSupportedErrorthrough theerrorproperty rather than throwing. Firefox does not expose the clipboard permission names, soreadPermission/writePermissionfall back to"unsupported"there.
Install
npm install @wcstack/clipboardQuick Start
1. Copy text (write)
Writes need a user gesture, so drive them from a DOM click (autoTrigger) or a command-token.
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/clipboard/auto"></script>
<wcs-clipboard id="cb"></wcs-clipboard>
<!-- Optional DOM triggering: click copies the literal text -->
<input id="token" value="abc-123" readonly />
<button data-clipboardtarget="cb" data-clipboard-from="#token">Copy</button>
<button data-clipboardtarget="cb" data-clipboard-text="Hello!">Copy greeting</button>data-clipboard-text copies a literal string; data-clipboard-from copies the value (or textContent) of the element matched by the selector.
2. Copy from state (command-token)
<wcs-state>
<script type="module">
export default {
message: "Shareable link",
$commandTokens: ["copy"],
onShare() { this.$command.copy.emit(this.message); }
};
</script>
</wcs-state>
<wcs-clipboard data-wcs="command.writeText: $command.copy"></wcs-clipboard>
<button data-wcs="onclick: onShare">Share</button>3. Read text (paste on demand, command-token)
The DOM autoTrigger only drives writes (writeText); there is no DOM-trigger
path for reads. Drive a read from a command-token, or call readText() /
read() imperatively on the element.
<wcs-state>
<script type="module">
export default {
pasted: "",
busy: false,
$commandTokens: ["paste"],
onPaste() { this.$command.paste.emit(); }
};
</script>
</wcs-state>
<wcs-clipboard
data-wcs="command.readText: $command.paste; text: pasted; loading: busy"></wcs-clipboard>
<button data-wcs="onclick: onPaste">Paste</button>
<p data-wcs="textContent: pasted"></p>Reading requires focus + read permission; the browser may prompt. Bind
errorto handle a denied read.
4. Monitor the user's clipboard activity (event-token)
Add the monitor attribute to republish document copy / cut / paste as reactive state.
<wcs-state>
<script type="module">
export default {
lastPaste: "",
$eventTokens: ["clipboardPasted"],
$on: {
clipboardPasted: (state, event) => { state.lastPaste = event.detail; }
}
};
</script>
</wcs-state>
<wcs-clipboard monitor
data-wcs="pasted: lastPaste; eventToken.pasted: clipboardPasted"></wcs-clipboard>Attributes / Inputs
| Attribute | Type | Default | Description |
| --------- | ------- | ------- | --------------------------------------------------------------------------- |
| monitor | boolean | false | Subscribe to document copy / cut / paste on connect and republish them as copied / cut / pasted. |
DOM trigger attributes (autoTrigger, copy-on-click)
| Attribute | On | Description |
| --------------------- | -------------- | ---------------------------------------------------------------------- |
| data-clipboardtarget| trigger button | Id of the <wcs-clipboard> to drive. |
| data-clipboard-text | trigger button | Literal text to copy (takes precedence; empty string is valid). |
| data-clipboard-from | trigger button | CSS selector; copies the matched element's value (or textContent). |
DOM triggers are write-only: a click always drives
writeText. There is no DOM-trigger path for reads (readText/read) — drive reads from a command-token or imperatively.
A DOM-triggered
writeTextis fire-and-forget (itsPromiseis not awaited), but it never rejects: a failed copy surfaces through theerrorproperty like any other write. Binderror(e.g.text: error.message@cb) to observe autoTrigger failures.
Observable Properties (outputs)
| Property | Event | Description |
| ---------------- | ---------------------------------------- | -------------------------------------------------------------------- |
| text | wcs-clipboard:read | Plain text from the last readText() / read() (or null). |
| items | wcs-clipboard:read | Normalized ClipboardItem snapshot from read() ({ types, data }[]), or null. |
| loading | wcs-clipboard:loading-changed | true during any async read/write. |
| error | wcs-clipboard:error | Normalized { name, message } (e.g. NotAllowedError, NotSupportedError). |
| readPermission | wcs-clipboard:read-permission-changed | "prompt" / "granted" / "denied" / "unsupported" for clipboard-read. |
| writePermission| wcs-clipboard:write-permission-changed | Same states for clipboard-write. |
| monitoring | wcs-clipboard:monitoring-changed | true while monitoring document clipboard events. |
| copied | wcs-clipboard:copied | Text of the latest monitored copy (from the selection). |
| cut | wcs-clipboard:cut | Text of the latest monitored cut. |
| pasted | wcs-clipboard:pasted | text/plain of the latest monitored paste. |
Commands
| Command | Description |
| ------------- | ------------------------------------------------------------------------- |
| writeText | Write a string to the clipboard (async; never rejects — failures go to error). Needs a user gesture. |
| write | Write ClipboardItem[] (images, HTML, multiple MIME types) (async). |
| readText | Read plain text; publishes text and wcs-clipboard:read (async). |
| read | Read rich ClipboardItems, resolving each representation to a Blob (async). |
| startMonitor| Begin monitoring document copy / cut / paste (no-op if already monitoring). |
| stopMonitor | Stop monitoring; monitoring becomes false. |
State-driven invocation uses the command-token protocol:
<wcs-clipboard data-wcs="command.writeText: $command.copy"></wcs-clipboard>Notes & limitations
- Attributes are read at connect time, not observed.
<wcs-clipboard>does not implementobservedAttributes/attributeChangedCallback. Themonitorattribute is read when the element connects — toggling it imperatively after connect does not start/stop monitoring by itself; callstartMonitor()/stopMonitor(), or re-connect the element. - No connect-time read. Unlike
<wcs-geo>, the clipboard cannot auto-read on connect (reads need a user gesture and permission), so there is noconnectedCallbackPromise/ SSR snapshot. The only connect-time action is optional monitoring. - Reconnect re-subscribes. Removing and re-inserting the element runs
connectedCallbackagain, so permission tracking is revived and amonitor-attribute element restarts monitoring (matching how it tears them down on disconnect). Monitoring persistence is attribute-driven only: if you started monitoring imperatively withstartMonitor()on an element without themonitorattribute, a reconnect does not restore it (the attribute is the source of truth). Add themonitorattribute for persistent monitoring across reparents. copy/cuttext comes from the selection. During acopy/cutevent the clipboard payload is not yet readable (the browser returns an empty string for security reasons), socopied/cutreportdocument.getSelection().toString()— the user's selected text. If the page installs a customcopyhandler that overrides the payload viaclipboardData.setData(...), that override is not reflected incopied/cut.pastedreadsevent.clipboardData.getData("text/plain").- Silent failure handling (zero-log). Consistent with the rest of wcstack's zero-dependency, minimal philosophy,
<wcs-clipboard>never logs or throws for runtime failures. A failed permission query (e.g. Firefox, which has no clipboard permission names) silently falls back to"unsupported". Read/write failures (denied permission, no focus, missing Clipboard API) are surfaced only through theerrorproperty /wcs-clipboard:errorevent — the commands resolve and never reject. Binderror(and the*Permissionproperties) to observe and react.
Headless usage (ClipboardCore)
The Core has no DOM dependency beyond the global document / navigator and can be used directly with bind() from @wc-bindable/core:
import { ClipboardCore } from "@wcstack/clipboard";
const clip = new ClipboardCore();
clip.addEventListener("wcs-clipboard:read", (e) => {
console.log((e as CustomEvent).detail); // { text, items }
});
await clip.writeText("hello");
await clip.readText();
// or, to monitor the user's clipboard activity:
clip.startMonitor();
// ...later
clip.stopMonitor();License
MIT
