Copy button
A small icon button that copies a string to the clipboard and confirms it for two seconds.
import { CopyButton } from '@/components/copy-button'
export default function Example() {
return <CopyButton text="sk_live_51H8kQ2..." />
}<script setup lang="ts">
import CopyButton from '@/components/copy-button.vue'
</script>
<template>
<CopyButton text="sk_live_51H8kQ2..." />
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
'use client'
import { CheckIcon, Square2StackIcon } from '@heroicons/react/16/solid'
import clsx from 'clsx'
import type React from 'react'
import { useEffect, useRef, useState } from 'react'
export function CopyButton({
text,
label = 'Copy',
className,
onClick,
...props
}: { text: string; label?: string } & Omit<React.ComponentPropsWithoutRef<'button'>, 'children'>) {
let [status, setStatus] = useState<'idle' | 'copied' | 'failed'>('idle')
let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
useEffect(() => () => clearTimeout(timeout.current), [])
async function copy(event: React.MouseEvent<HTMLButtonElement>) {
onClick?.(event)
try {
await navigator.clipboard.writeText(text)
setStatus('copied')
} catch {
setStatus('failed')
}
clearTimeout(timeout.current)
timeout.current = setTimeout(() => setStatus('idle'), 2000)
}
return (
<>
<button
type="button"
aria-label={label}
{...props}
onClick={copy}
className={clsx(
className,
'relative rounded-lg p-1 text-zinc-500 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-white'
)}
>
<span
className="absolute top-1/2 left-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden"
aria-hidden="true"
/>
{status === 'copied' ? (
<CheckIcon className="size-4 shrink-0 text-green-600 dark:text-green-400" aria-hidden="true" />
) : (
<Square2StackIcon className="size-4 shrink-0" aria-hidden="true" />
)}
</button>
<span className="sr-only" role="status">
{status === 'copied' ? 'Copied' : status === 'failed' ? 'Copy failed' : ''}
</span>
</>
)
}<script setup lang="ts">
import { CheckIcon, Square2StackIcon } from '@heroicons/vue/16/solid'
import { onBeforeUnmount, ref } from 'vue'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<{ text: string; label?: string }>(), {
label: 'Copy',
})
const status = ref<'idle' | 'copied' | 'failed'>('idle')
let timeout: ReturnType<typeof setTimeout> | undefined
async function copy() {
try {
await navigator.clipboard.writeText(props.text)
status.value = 'copied'
} catch {
status.value = 'failed'
}
clearTimeout(timeout)
timeout = setTimeout(() => (status.value = 'idle'), 2000)
}
onBeforeUnmount(() => clearTimeout(timeout))
</script>
<template>
<button
type="button"
:aria-label="label"
v-bind="$attrs"
class="relative rounded-lg p-1 text-zinc-500 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-white"
@click="copy"
>
<span class="absolute top-1/2 left-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" aria-hidden="true" />
<CheckIcon v-if="status === 'copied'" class="size-4 shrink-0 text-green-600 dark:text-green-400" aria-hidden="true" />
<Square2StackIcon v-else class="size-4 shrink-0" aria-hidden="true" />
</button>
<span class="sr-only" role="status">{{ status === 'copied' ? 'Copied' : status === 'failed' ? 'Copy failed' : '' }}</span>
</template>Component API
| Prop | Default | Description |
|---|---|---|
CopyButtonrenders a <button> element | ||
text | - | The string written to the clipboard when the button is pressed. |
label | 'Copy' | Accessible label, read by screen readers and never shown. The result, Copied or Copy failed, is announced after it. |
| Prop | Default | Description |
|---|---|---|
CopyButtonrenders a <button> element | ||
text | - | The string written to the clipboard when the button is pressed. |
label | 'Copy' | Accessible label, read by screen readers and never shown. The result, Copied or Copy failed, is announced after it. |
Examples
Basic example
Pass the string to copy through the text prop. The icon switches to a checkmark for two seconds after a successful copy:
import { CopyButton } from '@/components/copy-button'
export default function Example() {
return <CopyButton text="sk_live_51H8kQ2..." />
}<script setup lang="ts">
import CopyButton from '@/components/copy-button.vue'
</script>
<template>
<CopyButton text="sk_live_51H8kQ2..." />
</template>With a custom label
The button renders no visible text, so give it a label that says what is being copied:
import { CopyButton } from '@/components/copy-button'
export default function Example() {
return <CopyButton text="sk_live_51H8kQ2..." label="Copy API key" />
}<script setup lang="ts">
import CopyButton from '@/components/copy-button.vue'
</script>
<template>
<CopyButton text="sk_live_51H8kQ2..." label="Copy API key" />
</template>Next to a value
Pair the button with the value it copies, so the two read as one control:
npm install @headlessui/reactimport { Code } from '@/components/text'
import { CopyButton } from '@/components/copy-button'
export default function Example() {
return (
<div className="flex items-center gap-2">
<Code>npm install @headlessui/react</Code>
<CopyButton text="npm install @headlessui/react" label="Copy command" />
</div>
)
}<script setup lang="ts">
import Code from '@/components/code.vue'
import CopyButton from '@/components/copy-button.vue'
</script>
<template>
<div class="flex items-center gap-2">
<Code>npm install reka-ui</Code>
<CopyButton text="npm install reka-ui" label="Copy command" />
</div>
</template>The clipboard API is only available over HTTPS and on localhost.