CopyButton
Copy-to-clipboard button that cycles through ready, success and error states and
resets itself after reset_sec. Bind state to observe it, or pass labels to swap the
icon and tooltip per state. This docs site mounts one in global mode, which attaches a
button to every code block.
The example below covers all three states plus disabled behavior, the empty-content guard, and reset timing.
Copied: nothing yet
Callback events:
No callback events yet.
svelte<script lang="ts">
import { tooltip } from '$lib/attachments'
import { CopyButton } from '$lib'
type Copy_state = `ready` | `success` | `error`
let content = $state(`npm test`)
let disabled = $state(false)
let reset_sec = $state(2)
let state = $state<Copy_state>(`ready`)
let copied_value = $state(``)
let copy_error = $state(``)
let callback_events = $state<string[]>([])
const labels = {
ready: { icon: `Copy`, text: `ready` },
success: { icon: `Check`, text: `success` },
error: { icon: `Alert`, text: `error` },
} as const
const push_event = (event_name: string, payload: string): void => {
callback_events = [`${event_name}: ${payload}`, ...callback_events].slice(0, 5)
}
const handle_copy_success = (copied_content: string): void => {
copied_value = copied_content
copy_error = ``
push_event(`on_copy_success`, copied_content || `""`)
}
const handle_copy_error = (error: unknown): void => {
const error_message = String(error)
copy_error = error_message
push_event(`on_copy_error`, error_message)
}
</script>
<p style="display: flex; gap: 6pt; align-items: center; flex-wrap: wrap; margin: 0 0 8pt">
<CopyButton
{content}
bind:state
{disabled}
{reset_sec}
{labels}
on_copy_success={handle_copy_success}
on_copy_error={handle_copy_error}
/>
<input bind:value={content} style="min-width: 16em" />
</p>
<div
style="display: flex; gap: 12pt; align-items: center; flex-wrap: wrap; margin: 0 0 8pt; font-size: 0.9em"
>
<label>
<span
style="cursor: help; text-decoration: underline dotted"
{@attach tooltip({ content: `Zero or negative values disable auto-reset` })}
>
reset_sec:
</span>
<input type="number" min="-1" step="0.5" bind:value={reset_sec} style="width: 5em" />
</label>
<label>
<input type="checkbox" bind:checked={disabled} />
disabled
</label>
</div>
<p><strong>Copied:</strong> {copied_value || `nothing yet`}</p>
{#if copy_error}
<p><strong>Error:</strong> {copy_error}</p>
{/if}
<p style="margin: 8pt 0 0"><strong>Callback events:</strong></p>
{#if callback_events.length}
<ul style="margin: 6pt 0 0; padding-left: 1.25em">
{#each callback_events as callback_event}
<li><code>{callback_event}</code></li>
{/each}
</ul>
{:else}
<p style="margin: 6pt 0 0; opacity: 0.8">No callback events yet.</p>
{/if}