Copy Button
Accessible, unstyled clipboard copy button primitive.
| Package | @vizejs/ui/copy-button |
| Maturity | stable |
| Own the source | vize lib pull copy-button |
| Requires | — |
| Aliases | copy button, clipboard button, copy to clipboard, clipboard action |
| Covers | shadcn/ui Copy Button, React Aria Button, WAI-ARIA Button |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { CopyButton } from "@vizejs/ui/copy-button";
Or copy the source into your project with vize lib pull copy-button (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Copies an invite link to the clipboard and announces the copied state in its label. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import { CopyButton } from "@vizejs/ui/copy-button";
const inviteLink = "https://example.com/invite/team-alpha";
const linkId = useId();
const lastCopied = ref("");
</script>
<template>
<div>
<code :id="linkId">{{ inviteLink }}</code>
<CopyButton
:value="inviteLink"
idle-label="Copy invite link"
copied-label="Invite link copied"
error-label="Could not copy"
:aria-describedby="linkId"
@copy="(value) => (lastCopied = value)"
/>
<p aria-live="polite">{{ lastCopied ? "Link is on your clipboard." : "" }}</p>
</div>
</template>
API
CopyButton
Source: src/families/actions/copy-button/copy-button.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
as |
PrimitiveAs |
"button" |
Native element, custom element, or component to render. |
native |
boolean |
true when |
Whether the rendered target already implements native button semantics. |
type |
CopyButtonType |
"button" |
Native button submission behavior. |
value (required) |
string |
required |
Plain string copied to the system clipboard on activation. |
disabled |
boolean |
false |
Remove the control from activation and sequential keyboard focus. |
idleLabel |
string |
"Copy" |
Idle fallback text rendered when no default slot is supplied. |
copiedLabel |
string |
"Copied" |
Success fallback text rendered when no default slot is supplied. |
errorLabel |
string |
"Copy failed" |
Failure fallback text rendered when no default slot is supplied. |
ariaLabel |
string |
undefined |
Accessible name when no visible label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the button. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the button. |
writer |
CopyButtonWriter |
navigator.clipboard.writeText |
Test or platform hook for writing clipboard text. |
Events
| Event | Payload | Description |
|---|---|---|
copy |
[value: string, nativeEvent: MouseEvent] |
Fired after the configured writer accepts the value. |
error |
[error: unknown, value: string, nativeEvent: MouseEvent] |
Fired after the configured writer rejects the value. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
CopyButtonSlotState |
Renders button contents with the current copy state. |
Exposed
| Member | Type | Description |
|---|---|---|
value |
string |
Plain string that will be passed to the writer on activation. |
disabled |
boolean |
Whether the control suppresses user activation. |
writing |
boolean |
Whether an async write is already in flight. |
unavailable |
boolean |
Whether activation is currently unavailable. |
state |
CopyButtonState |
Stable copy result token for styling and tests. |
label |
string |
Resolved fallback text for the current state. |
element |
CopyButtonElement | null |
Rendered root element or component instance. |
focus |
(options?: FocusOptions) => void |
Move focus to the rendered control. |
Behavior
Normative state × input → outcome table for copy-button.vue.
| ID | Scenario | Behavior | Assertion |
|---|---|---|---|
| C1 | native render | renders a native button with type="button", part="root", label part, and idle state |
renders deterministic native button semantics and default label |
| C2 | default clipboard | activation writes the string through navigator.clipboard.writeText when available |
uses navigator.clipboard.writeText by default |
| C3 | successful write | accepts the configured writer, emits copy, and exposes data-state="copied" |
copies the configured value and exposes copied state |
| C4 | rejected write | catches writer failures, emits error, and exposes data-state="error" |
captures writer failures without throwing out of activation |
| C5 | disabled | native disabled removes activation; non-native disabled leaves tab order with ARIA state | disabled copy buttons suppress writes and keep platform semantics |
| C6 | labels and slots | label props and default slot receive the same strict idle | copied | error state |
supports custom labels and slot rendering |
| C7 | duplicate activation | a write in flight sets busy hooks and suppresses additional accidental writes | suppresses duplicate writes while a copy is in flight |
| C8 | non-native activation | non-native hosts expose button semantics and emulate native Enter/Space timing | non-native hosts preserve keyboard button activation |
| C9 | public instance | exposes live state, writing, unavailable, value, element, label, and focus() |
exposes live state and focus without broad clipboard abstractions |
| C10 | SSR and hydration | setup never touches browser globals; server markup is stable and hydrates without warnings | copy-button-ssr.test.ts and runtime conformance fixtures |
Contract
CopyButton is a headless action primitive for copying one plain string value.
It intentionally owns only a narrow clipboard write: the default writer calls
navigator.clipboard.writeText(value) at activation time, and tests or SSR
consumers may inject a writer prop. Component setup, SSR rendering, and
hydration do not access navigator, window, timers, or document globals.
The result state is closed to idle, copied, and error. Disabled and
in-flight writes are separate availability hooks through data-disabled,
data-writing, aria-disabled, aria-busy, slot state, and the public expose
contract. In-flight writes preserve focus rather than applying native disabled.
The rendered DOM exposes part="root" on the host and part="label" on the
fallback label span. Stable selectors are data-vize-ui="copy-button" and
data-vize-ui="copy-button-label". No CSS classes, runtime styles, CSS
custom properties, runtime CSS-in-JS, or copied value data attributes are
emitted; all visual styling remains consumer-owned.