Copy Button

One-click copy to clipboard with a friendly confirmation.

zweiundeins <sb-copy-button> clipboard copy code Since 2026-09-21 1.7 kB0Open in playground Edit on GitHub

Playground

A small icon button that copies its value to the clipboard and confirms with a check mark, or says so when the browser refuses. Every code block on this site uses it.

Examples

Basic

<sb-button>Launch</sb-button>
<code>&lt;sb-button&gt;Launch&lt;/sb-button&gt;</code>
<sb-copy-button value="<sb-button>Launch</sb-button>"></sb-copy-button>

Copy a Datastar signal

Bind value to a signal with data-attr, and the button always copies the current value.

<div data-signals:_coords="'51.4779° N, 0.0015° W'">
  <span data-text="$_coords"></span>
  <sb-copy-button data-attr:value="$_coords" label="Copy coordinates"></sb-copy-button>
</div>

React to copies

The sb-copy event bubbles out of the shadow root, so data-on works on any ancestor.

<div data-signals:_copies="0" data-on:sb-copy="$_copies++">
  <sb-copy-button value="ignition"></sb-copy-button>
  <span data-text="'Copied ' + $_copies + ' times'"></span>
</div>

When copying fails

Browsers can refuse the clipboard: outside a secure context (plain http://), in an iframe without the clipboard-write permission, or when the document isn't focused. The button then shows a cross and failed-label, and emits sb-copy-error with { value, error } (e.g. "NotAllowedError"), so a page can offer another way, like selecting the text:

<sb-copy-button value="go run ." data-on:sb-copy-error="console.warn('copy refused:', evt.detail.error)"></sb-copy-button>

Accessibility

The button's accessible name is label. The result ("Copied!" or "Copy failed") goes to a role="status" region next to the button, so screen readers announce it without moving focus. It sits outside the button on purpose: a button's children are presentational, so a live region inside one may never be read out. The same element is the visual tip (::part(tip)).

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag. The autoloader imports each component the first time its tag appears, including tags added later by a Datastar morph.

<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
     It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>

<div style="display: flex; align-items: center; gap: 8px"><code>go run .</code><sb-copy-button value="go run ."></sb-copy-button></div>

<!-- In production, pin today's catalog instead of the latest: the browser then
     refuses any file that changed. Add "integrity" to the import map above: the
     hashes from https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/autoloader.js" integrity="sha384-IvSCUjEWPS+wwXunU+48x96KmyWDx/Y4LeCwCL8cWFqJ/zHmTsTOsSMrzTOcpW3D"></script>
-->

<!-- Or load just this component, pinned to this version: -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/copy-button@63dcadb0a39b/copy-button.js" integrity="sha384-V+FUHNRnsITpz/2n9wPk/L6C0xlDcKorzJ2ex/tT6K47XyQKZYYt+x5jpa++V9/D"></script> -->

Size

Each file compressed on its own, the way it is served (gzip -9, brotli -11). Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotli
copy-button.js 5.0 kB2.0 kB1.7 kB

API reference

Props

AttributeTypeDefaultDescription
valuestring""The text copied to the clipboard.
labelstring"Copy to clipboard"Accessible label of the button.
copied-labelstring"Copied!"Shown and announced after copying.
failed-labelstring"Copy failed"Shown and announced when the browser refuses the clipboard (no secure context, permissions policy…).
reset-msnumber1600How long the copied or failed state lasts, in ms.

Events

NameDescription
sb-copyAfter copying. detail: { value }.
sb-copy-errorWhen the browser refuses the clipboard write. detail: { value, error } (the error's name, e.g. "NotAllowedError").