UIPackage
Menu

Clipboard

clipboard ui
Edit on GitHub

Copy-to-clipboard button with success/error feedback. Swaps the icon to a check on success, shows a tooltip with configurable feedback text, supports a visible label, disabled state, a custom timeout for feedback reset, and copy/success/error events. Includes a legacy execCommand fallback for non-secure contexts. Composes the tooltip primitive.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/clipboard.json
Named registry: npx shadcn-vue@latest add @uipkge/clipboard Installs to: app/components/ui/clipboard/

Examples

Props

Name Type / Values Default Required
text

Text to copy to the clipboard.

string '' optional
label

Optional visible label next to the icon.

string '' optional
disabled

Disable the button (no copy, no tooltip).

boolean false optional
hideIcon

Hide the copy icon (useful when a label is shown).

boolean false optional
tooltip

Tooltip text shown on hover before copying.

string 'Copy' optional
successText

Feedback text shown after a successful copy.

string 'Copied!' optional
errorText

Feedback text shown after a failed copy.

string 'Failed' optional
timeout

How long (ms) the success/error feedback stays before resetting.

number 2000 optional
feedbackTooltip

Show the feedback as a tooltip rather than swapping the icon.

boolean true optional
class HTMLAttributes['class'] optional

npm dependencies

Includes

Files installed (2)

  • app/components/ui/clipboard/Clipboard.vue 3.9 kB
    <script setup lang="ts">
    import { computed, onBeforeUnmount, ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { Check, Copy } from 'lucide-vue-next'
    import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
    import { cn } from '@/lib/utils'
    
    const props = withDefaults(
      defineProps<{
        /** Text to copy to the clipboard. */
        text?: string
        /** Optional visible label next to the icon. */
        label?: string
        /** Disable the button (no copy, no tooltip). */
        disabled?: boolean
        /** Hide the copy icon (useful when a label is shown). */
        hideIcon?: boolean
        /** Tooltip text shown on hover before copying. */
        tooltip?: string
        /** Feedback text shown after a successful copy. */
        successText?: string
        /** Feedback text shown after a failed copy. */
        errorText?: string
        /** How long (ms) the success/error feedback stays before resetting. */
        timeout?: number
        /** Show the feedback as a tooltip rather than swapping the icon. */
        feedbackTooltip?: boolean
        class?: HTMLAttributes['class']
      }>(),
      {
        text: '',
        label: '',
        disabled: false,
        hideIcon: false,
        tooltip: 'Copy',
        successText: 'Copied!',
        errorText: 'Failed',
        timeout: 2000,
        feedbackTooltip: true,
      },
    )
    
    const emit = defineEmits<{
      (e: 'copy', text: string): void
      (e: 'success', text: string): void
      (e: 'error', error: Error): void
    }>()
    
    type State = 'idle' | 'success' | 'error'
    const state = ref<State>('idle')
    let resetTimer: ReturnType<typeof setTimeout> | null = null
    
    function setFeedback(nextState: State) {
      state.value = nextState
      if (resetTimer) clearTimeout(resetTimer)
      resetTimer = setTimeout(() => {
        state.value = 'idle'
      }, props.timeout)
    }
    
    async function copy() {
      if (props.disabled) return
      const value = props.text
      emit('copy', value)
      try {
        if (navigator.clipboard?.writeText) {
          const write = navigator.clipboard.writeText(value)
          setFeedback('success')
          await write
        } else {
          // Legacy fallback for non-secure contexts.
          const ta = document.createElement('textarea')
          ta.value = value
          ta.style.position = 'fixed'
          ta.style.opacity = '0'
          document.body.appendChild(ta)
          ta.select()
          document.execCommand('copy')
          document.body.removeChild(ta)
          setFeedback('success')
        }
        emit('success', value)
      } catch (err) {
        setFeedback('error')
        emit('error', err as Error)
      }
    }
    
    const currentTooltip = computed(() => {
      if (state.value === 'success') return props.successText
      if (state.value === 'error') return props.errorText
      return props.tooltip
    })
    
    onBeforeUnmount(() => {
      if (resetTimer) clearTimeout(resetTimer)
    })
    </script>
    
    <template>
      <TooltipProvider :delay-duration="300">
        <Tooltip>
          <TooltipTrigger as-child>
            <button
              type="button"
              data-uipkge
              data-slot="clipboard"
              :data-feedback-state="state"
              :disabled="disabled"
              :class="
                cn(
                  'inline-flex items-center gap-1.5 rounded-md text-sm transition-colors',
                  'text-muted-foreground hover:text-foreground',
                  'focus-visible:ring-ring/50 outline-none focus-visible:ring-[3px]',
                  'disabled:cursor-not-allowed disabled:opacity-50',
                  props.class,
                )
              "
              :aria-label="currentTooltip"
              @click="copy"
            >
              <span v-if="!hideIcon" data-slot="clipboard-icon" class="inline-flex">
                <Check v-if="state === 'success'" class="size-4 text-emerald-500" />
                <Copy v-else class="size-4" />
              </span>
              <span v-if="label" data-slot="clipboard-label">{{ label }}</span>
              <slot :state="state" />
            </button>
          </TooltipTrigger>
          <TooltipContent v-if="feedbackTooltip || state === 'idle'">
            {{ currentTooltip }}
          </TooltipContent>
        </Tooltip>
      </TooltipProvider>
    </template>
  • app/components/ui/clipboard/index.ts 0.1 kB
    export { default as Clipboard } from './Clipboard.vue'

Raw manifest: https://uipkge.dev/r/vue/clipboard.json