Copy
Copy — a button that puts text on the clipboard, and tells you when it could not.
Examples
npm install mostlycss
<figure class="code">
<pre class="code-body" tabindex="0" role="region" aria-label="Installing from npm"><code id="install-code">npm install mostlycss</code></pre>
</figure>
<button class="btn btn-sm" type="button"
data-ui="copy" data-copy-from="#install-code">Copy</button>Two attributes and no proximity magic: data-copy-from takes a selector, data-copy-text takes the literal string.
Clipboard access is refused on insecure origins and when the document is not focused, so the failure path is real — a button saying "Copied" over an empty clipboard is worse than one that admits it did not work.
<button class="btn btn-sm" type="button"
data-ui="copy" data-copy-text="sk_live_9f3a">Copy key</button>The confirmation is the button’s own label changing. It is what has focus at that moment, so a screen reader announces it without a second live region saying the same thing twice.
It is also a claim about a value, so it is withdrawn when the value stops being true: while a message is showing the module watches whatever data-copy-from points at — through input, change and a mutation observer, so a rewritten textContent counts even though it fires no event — and for a value that never touches the DOM, the registry’s refresh(button) is the way out. Copy a generated password, press Regenerate, and “Copied” must not still be sitting under a password that is no longer on the clipboard.
<button class="btn btn-sm" type="button"
data-ui="copy" data-copy-text="sk_live_9f3a" aria-label="Copy the API key">
<svg class="icon" aria-hidden="true"><use href="#icon-copy"></use></svg>
<span data-copy-label>Copy key</span>
</button>data-copy-label is required the moment the button contains an element rather than plain text, and leaving it out is the defect this example exists to prevent: the module will not write over a button with children — that would delete your icon — and it cannot guess which child is the label, so the visible word never changes.
The accessible name still does, which makes it worse rather than better: a screen reader user is told and a sighted user is not, and neither side looks broken on its own. Press it: the word swaps, the icon stays.
Tokens 0
Level 2, declared on .copy itself. Set any of them on that selector to restyle this component without touching the skin.
None. It reads the skin's tokens directly.
Variants and states
Variants
None. It is one shape, and the page does the rest.
State it reads
None.
Before you ship it
This one needs a script. Opt the markup in with data-ui="copy" and import import 'mostlycss/js/components/copy' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.
What you have to do 5 requirements
- Give the button a name that describes what is copied, not the widget: "Copy the install command", not "Copy". The module renames the button for the confirmation window and puts your name back afterwards, so the name it starts from is the one a screen reader reads most of the time.
- Provide the text to copy on the button —
data-copy-textfor a literal string,data-copy-fromfor a selector — rather than relying on a selection the user made. (This bullet nameddata-copyanddata-copy-targetfor a while. Neither has ever existed: the module refuses to initialise without one of the two real ones and says so, but an agent reading the contract wrote the wrong attribute first.) - If the value can change while the confirmation is showing, make sure it is withdrawn. "Copied" under a value that is no longer the one on the clipboard is the component lying on your behalf, and it is the case a generated password finds within a minute: copy, dislike, press Regenerate. Usually there is nothing to do — the module watches the element named by
data-copy-fromwhile a message is on screen, through bothinput/changeand a mutation observer, so a rewrittentextContentis caught even though it fires no event. Where the new value never touches the DOM, call the registry'srefresh(): import 'mostlycss/js/components/copy'; import { refresh } from 'mostlycss/js/core/registry'; regenerate(); refresh(copyButton); Takerefreshfrommostlycss/js/core/registry, not frommostlycss. The two are not the same registry: the package root is a bundle with its own copy compiled into it, so a page that imports this component frommostlycss/js/…andrefreshfrommostlycssgets arefreshthat silently does nothing to it — and a second instance on every button, which writes the clipboard twice per click. Do not put the label back by hand. The module ownsaria-label,aria-labelledby, the visible text anddata-copy-statefor the length of the window, and a page that restores one of them is racing a timer it cannot see. - Do not add a
role="status"region beside it. The announcement comes from the change to the focused button's accessible name; a second mechanism says the same string twice. - If the button contains an element — an icon, a
<span>, anything — putdata-copy-labelon the node holding the text, or nothing visible will ever confirm the copy. This module will not writetextContenton a button that has children, because that deletes the icon somebody put there, and it cannot guess which child is the label: <button class="btn" data-ui="copy" data-copy-text="npm i mostlycss"> <svg class="icon" aria-hidden="true"><use href="#icon-copy"></use></svg> <span data-copy-label>Copy</span> </button> Without it the button reads "Copy" for ever. The accessible name is still swapped, so a screen reader user is told and a sighted user is not — the worst shape a defect can have, and it looks correct from either side on its own. Three buttons shipped that way on one page, and the only symptom was the absence of a change.
src/js/components/copy.ts · npx mostlycss add copy