Copy Button

Copies a text value to the clipboard and announces the result to screen readers. Each instance copies its own value.

Source on GitHub

Usage

text is the exact text to copy. label is the button text and defaults to "Copy".

examples/components/copy-button/copy-button.html
html
<copy-button data-bascik-prop-text="npm create bascik@latest" data-bascik-prop-label="Copy command" />
html examples/components/copy-button/copy-button.html
<!-- @bascik
Copies a text value to the clipboard and announces the result to screen readers.
@prop text - The exact text to copy.
@prop label - Button text. Defaults to "Copy".
-->
<style>
  .copy-button {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
  }

  .button {
    padding: 0.25rem 0.75rem;
    border: 1px solid var(--color-border, color-mix(in srgb, currentColor 30%, transparent));
    border-radius: var(--radius, 0.5rem);
    background: var(--color-surface, color-mix(in srgb, currentColor 8%, transparent));
    color: inherit;
    font: inherit;
    cursor: pointer;
  }

  .button:hover {
    border-color: var(--color-accent, currentColor);
  }

  .status {
    color: var(--color-muted, inherit);
    font-size: 0.875em;
  }
</style>
<span class="copy-button">
  <button class="button" id="button" type="button" data-bascik-attr-data-text="text">
    <span data-bascik-prop-label>Copy</span>
  </button>
  <span class="status" id="status" role="status"></span>
</span>
<script>
  const button = document.getElementById('button');
  const status = document.getElementById('status');
  let timer;

  button.addEventListener('click', async () => {
    try {
      await navigator.clipboard.writeText(button.dataset.text ?? '');
      status.textContent = 'Copied';
    } catch {
      // Clipboard access needs a secure context (HTTPS or localhost) and can be denied.
      status.textContent = 'Copy failed. Select the text and copy it manually.';
    }
    clearTimeout(timer);
    timer = setTimeout(() => {
      status.textContent = '';
    }, 2000);
  });
</script>
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.

How It Works

  • data-bascik-attr-data-text="text" binds the text prop to the button's data-text attribute, and the script reads button.dataset.text. The value travels with each instance, so there is no shared state.
  • navigator.clipboard.writeText() needs a secure context (HTTPS or localhost) and can be denied. Both outcomes are written to a role="status" element, which screen readers announce, and cleared after two seconds.

Adapt It

  • To copy the text of a code block, read textContent from an element the component finds by ID instead of a prop.
  • Show the result as an icon change instead of text, but keep the role="status" message for screen readers.