Copy

button.btn[type="button"][data-ui="copy"][data-copy-from="#install-code"]

Copy — a button that puts text on the clipboard, and tells you when it could not.

Examples

Copy what is on the page

View “Copy what is on the page” as
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.

Copy something not on the page

View “Copy something not on the page” as
<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.

With an icon, and the attribute it then needs

View “With an icon, and the attribute it then needs” as
<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

  1. 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.
  2. Provide the text to copy on the buttondata-copy-text for a literal string, data-copy-from for a selector — rather than relying on a selection the user made. (This bullet named data-copy and data-copy-target for 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.)
  3. 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-from while a message is on screen, through both input/change and a mutation observer, so a rewritten textContent is caught even though it fires no event. Where the new value never touches the DOM, call the registry's refresh(): import 'mostlycss/js/components/copy'; import { refresh } from 'mostlycss/js/core/registry'; regenerate(); refresh(copyButton); Take refresh from mostlycss/js/core/registry, not from mostlycss. 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 from mostlycss/js/… and refresh from mostlycss gets a refresh that 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 owns aria-label, aria-labelledby, the visible text and data-copy-state for the length of the window, and a page that restores one of them is racing a timer it cannot see.
  4. 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.
  5. If the button contains an element — an icon, a <span>, anything — put data-copy-label on the node holding the text, or nothing visible will ever confirm the copy. This module will not write textContent on 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