Clipboard
A navigator.clipboard.writeText wrapper with a copied state that auto-resets after a configurable timeout. Consumers compose a Trigger button (copy on click) and an optional Indicator that renders only while the copy is fresh.
navigator.clipboard is required — there is no execCommand('copy') fallback. 98%+ of browsers in 2026 support the async API, and the legacy API's synchronous-gesture requirement is incompatible with Svelte 5's microtask-based event handling.
Anatomy
<Clipboard.Provider value="npm install soma">
<Clipboard.Trigger>Copy</Clipboard.Trigger>
<Clipboard.Indicator>Copied!</Clipboard.Indicator>
</Clipboard.Provider>
Parts
| Part |
Element |
Description |
Provider |
<div> |
Root context. Holds value + copied + imperative copy(). |
Trigger |
<button> |
Copies on click. Announces data-copied for visual feedback. |
Indicator |
<span> |
Renders only while copied=true (unless forceMount). Decorative. |
Props
Provider
| Prop |
Type |
Default |
Description |
id |
string |
auto |
DOM id. |
value |
string |
— |
Required. String to copy. |
timeout |
number |
2000 |
Ms to keep copied=true after a successful copy. |
onCopy |
(value: string) => void |
— |
Fires on successful copy. |
onError |
(err: unknown) => void |
— |
Fires when the clipboard API throws (permissions, blur, missing API). |
Snippet props: { value, copied, copy }.
Trigger
| Prop |
Type |
Default |
Description |
id |
string |
auto |
DOM id. |
aria-label |
string |
translated Copy to clipboard / Copied (swaps with state) |
Accessible name. |
Indicator
| Prop |
Type |
Default |
Description |
id |
string |
auto |
DOM id. |
forceMount |
boolean |
false |
Keep in DOM even when copied=false, for CSS exit transitions. |
ARIA
| Part |
Attribute |
Value |
| Trigger |
type |
button |
| Trigger |
aria-label |
Translated Copy to clipboard → Copied on transition |
| Trigger |
aria-live |
polite — announces the label change |
| Indicator |
aria-hidden |
true (decorative; the Trigger's label is the source of truth) |
Data Attributes
| Part |
Attribute |
Values |
| Provider |
data-clipboard |
Always present |
| Provider |
data-copied |
Present (empty string) while fresh |
| Trigger |
data-clipboard-trigger |
Always present |
| Trigger |
data-copied |
Present while fresh |
| Indicator |
data-clipboard-indicator |
Always present when rendered |
| Indicator |
data-copied |
Always present (Indicator only renders while copied) |
Keyboard
| Key |
Action |
Enter / Space |
Clicks the Trigger → calls copy() |
i18n
| Key |
English |
Spanish |
copy |
Copy to clipboard |
Copiar al portapapeles |
copied |
Copied |
Copiado |
Override per-instance via aria-label on Trigger.
Comparison
| Feature |
Soma |
Radix |
Ark UI |
bits-ui |
| Dedicated component |
✅ |
❌¹ |
✅ |
❌ |
navigator.clipboard.writeText |
✅ |
— |
✅ |
— |
Auto-reset copied flag |
✅ |
— |
✅ |
— |
Configurable timeout |
✅ |
— |
✅ |
— |
onCopy / onError callbacks |
✅ |
— |
✅ |
— |
| Indicator conditional mount |
✅ |
— |
✅ |
— |
data-copied attr |
✅ |
— |
✅ |
— |
aria-live on Trigger label |
✅ |
— |
❌ |
— |
| Translated label swap |
✅ |
— |
❌ |
— |
execCommand fallback |
❌² |
— |
❌ |
— |
¹ Radix does not ship a Clipboard primitive. Consumers roll their own navigator.clipboard call + useState(copied).
² Dropped by design — see the intro note. The legacy API requires a synchronous gesture path that Svelte 5 does not provide.
Usage
Button with state swap
<Clipboard.Provider value={code}>
{#snippet children({ copied })}
<Clipboard.Trigger>
{copied ? '✓ Copied!' : 'Copy'}
</Clipboard.Trigger>
{/snippet}
</Clipboard.Provider>
Separate Indicator (icon swap)
<Clipboard.Provider value={code}>
<Clipboard.Trigger>
<CopyIcon />
</Clipboard.Trigger>
<Clipboard.Indicator>
<CheckIcon />
</Clipboard.Indicator>
</Clipboard.Provider>
Imperative copy (no button)
<Clipboard.Provider value={shareUrl}>
{#snippet children({ copy, copied })}
<p>Link copied automatically</p>
<button onclick={() => copy()}>Share</button>
{#if copied}<span role="status">Done!</span>{/if}
{/snippet}
</Clipboard.Provider>
Styling with data-copied
[data-clipboard-trigger] {
transition: background 150ms;
}
[data-clipboard-trigger][data-copied] {
background: #dcfce7;
color: #166534;
}
Error handling
<script>
let error = $state<string | null>(null);
</script>
<Clipboard.Provider
value={code}
onError={(err) => {
error = err instanceof Error ? err.message : 'Could not copy';
}}
>
<Clipboard.Trigger>Copy</Clipboard.Trigger>
</Clipboard.Provider>
{#if error}<p role="alert">{error}</p>{/if}
Permission denial (e.g. the browser blocks the API in insecure contexts) reaches onError; the Provider doesn't swallow it.