Copy Button

A button that copies text to the clipboard, its icon swapping for a check.

<script setup lang="ts">
import { Copy01Icon, Tick02Icon } from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/vue'
import { CopyButton, CopyButtonIcon } from '@animate-ui/vue/primitives/buttons/copy'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from 'cn'

const copyButtonVariants = cva(
  "focus-visible:border-ring focus-visible:ring-ring/50 inline-flex shrink-0 items-center justify-center rounded-lg transition-[color,background-color,border-color,box-shadow] outline-none focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
        destructive:
          'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white',
        outline:
          'bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
        ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50',
        link: 'text-primary underline-offset-4 hover:underline',
      },
      size: {
        default: 'size-9',
        xs: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
        sm: 'size-8',
        lg: 'size-10',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  },
)

type CopyButtonVariants = VariantProps<typeof copyButtonVariants>

withDefaults(
  defineProps<{
    variant?: CopyButtonVariants['variant']
    size?: CopyButtonVariants['size']
  }>(),
  { variant: 'default', size: 'default' },
)
</script>

<template>
  <CopyButton
    content="Hello world!"
    aria-label="Copy"
    :class="cn(copyButtonVariants({ variant, size }))"
  >
    <CopyButtonIcon>
      <template #icon>
        <HugeiconsIcon :icon="Copy01Icon" :stroke-width="2" />
      </template>
      <template #copied-icon>
        <HugeiconsIcon :icon="Tick02Icon" :stroke-width="2" />
      </template>
    </CopyButtonIcon>
  </CopyButton>
</template>

Installation

npm install @animate-ui/vue motion-v
pnpm add @animate-ui/vue motion-v
yarn add @animate-ui/vue motion-v
bun add @animate-ui/vue motion-v

Usage

<script setup lang="ts">
import { CopyButton, CopyButtonIcon } from '@animate-ui/vue/primitives/buttons/copy';
</script>

<template>
  <CopyButton content="npx animate-ui init" aria-label="Copy">
    <CopyButtonIcon>
      <template #icon><CopyIcon /></template>
      <template #copied-icon><CheckIcon /></template>
    </CopyButtonIcon>
  </CopyButton>
</template>

A click writes content to the clipboard (navigator.clipboard.writeText). Once it's written, the button is copied (data-copied) for delay milliseconds, then goes back. Copying again restarts the delay. Read the state in your own parts with useCopyButton().

Animation

  • Once copied, the copy icon shrinks to half its size into a light blur (blur(2px)) as it fades out, while the check grows out of the same blur in its place, over 0.15s with an ease-out (cubic-bezier(0.23, 1, 0.32, 1)). It swaps back the same way after delay. Nothing animates on mount.
  • Pressing scales the button to tap-scale (0.97), on Motion's spring for scale (stiffness: 550, damping: 30). It doesn't scale on hover, unless you set hover-scale.
  • <AnimateUIProvider :tempo> paces the swap and the press spring (not delay).
  • Under reduced motion, the icons crossfade through their blur without scaling, and the button doesn't scale.

API Reference

CopyButton

PropTypeDefault
content
string-
copied?
boolean-
default-copied?
booleanfalse
@copied-change?
(copied: boolean, content?: string) => void-
delay?
number3000
hover-scale?
number1
tap-scale?
number0.97
transition?
Transition{ type: 'spring', stiffness: 550, damping: 30, restSpeed: 10 }
as-child?
booleanfalse
...props?
HTMLMotionProps<'button'>-

CopyButtonIcon

PropTypeDefault
#icon
slot-
#copied-icon
slot-
transition?
Transition{ duration: 0.15, ease: [0.23, 1, 0.32, 1] }
...props?
HTMLAttributes-

Data attributes

PropTypeDefault
data-copied?
present-

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/23/2026