@ozjsey/v-copy

Vue 3 directive that copies any element — and keeps the last N copies so your users can pick one back out. Copy history with de-duplication, a copied-feedback state, slot-like reactive state, keyboard + a11y, and an automatic clipboard fallback. Zero depe
1,097
Install
npm install @ozjsey/v-copy
Latest Version:1.2.3
Vue:3
License:MIT
Last Updated:Sep 20, 2026
Links: GitHub  ·  npm
Maintainer: ozjsey

@ozjsey/v-copy

A Vue 3 directive that copies any element — and keeps the last N copies so your users can pick one back out.

npm license MIT gzipped 4.39 KiB dependencies 0

The problem

Making one value copyable is a handler, a ref, a writeText, a copied flag and a timer to clear it — and none of that makes the <li> keyboard-operable or tells a screen reader anything happened. Write it a dozen times for a table of addresses and the user still loses: they copy the next row, and the one they actually wanted is gone. The clipboard holds one value and your UI kept no record of the rest.

The solution

Drop v-copy on a <li>, a <code>, a table cell. Click it — or Tab to it and press Enter — and its text is on the clipboard, with a "Copied!" state and a screen-reader announcement you did not have to write.

<li v-for="email in emails" :key="email" v-copy>{{ email }}</li>

Bind an array or a reactive object as well and every copy is recorded, newest-first, de-duplicated. Render that history and each row copies itself back out — a clipboard manager, built from one directive. VueUse's useClipboard writes; this one remembers. If one string on the clipboard is all you need, useClipboard is smaller and probably already in your project.

One surprise worth having on day one: an empty or null source is refused rather than written — writing "" would silently clear the user's clipboard while the UI flashed "Copied!" — but undefined cannot be refused, because Vue compiles v-copy and v-copy="somethingUndefined" to the same binding, so undefined has to keep meaning "copy my textContent".

Install

npm install @ozjsey/v-copy

Requires Vue 3.0 or newer: the directive imports watch, isReactive, isReadonly and toRaw, all of which shipped in 3.0, and nothing newer.

import { createApp } from 'vue'
import App from './App.vue'
import { VCopyPlugin } from '@ozjsey/v-copy'

createApp(App)
  .use(VCopyPlugin /*, { max: 20, dedupe: false, announce: false } */) // optional global defaults
  .mount('#app')

Or locally — import { vCopy } from '@ozjsey/v-copy', registered under the name copy. The directive is always v-copy, whatever the package is called.

Usage

Copy an element's text

<template>
  <button v-copy>Copy this label</button>
  <code v-copy>npm install @ozjsey/v-copy</code>
</template>

The textContent is read live at copy time and trimmed. On success the element carries a v-copy-copied class and a [data-copied] attribute for a moment, so the confirmation is CSS rather than a template branch:

[data-copied]:https://github.com/ozjsey/v-copy/blob/HEAD/:after { content: ' ✓ Copied'; color: green }

Keep a copy history

Bind a ref to an array. The directive fills it mutating in place, newest-first and capped, so history.splice(0) clears it and the binding keeps working. A repeat copy is promoted back to the top instead of filling the list with the same row.

<script setup lang="ts">
import { ref } from 'vue'

const users = [{ id: 1, email: 'ada@lovelace.dev' }, { id: 2, email: 'grace@hopper.dev' }]
const history = ref<string[]>([])
</script>

<template>
  <li v-for="u in users" :key="u.id" v-copy="history">{{ u.email }}</li>

  <aside>Last copied: {{ history[0] }}</aside>
</template>

A history the user can pick from

The reason the package exists. Nothing here is special-cased: a history row is an ordinary v-copy whose source is the stored text and whose sink is the same array.

<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import type { CopyController, CopyEntry } from '@ozjsey/v-copy'

const values = ['ada@lovelace.dev', '9f2c1ab', 'sk-demo-4417']
const open = ref(false)

// `sink: []` is where entries are written. `history` is populated by the
// directive's mounted hook, which Vue runs AFTER the first render — hence the
// `?? []` below rather than a promise that it is an array from the start.
const clipboard = reactive<CopyController>({ sink: [], max: 8 })
const isText = (e: CopyEntry): e is string => typeof e === 'string'
const rows = computed(() => (clipboard.history ?? []).filter(isText))
</script>

<template>
  <code v-for="v in values" :key="v" v-copy="clipboard">{{ v }}</code>

  <button @click="open = !open">History ({{ rows.length }})</button>

  <ul v-if="open">
    <!-- Each row writes back into the SAME history, so picking an old value puts
         it on the clipboard and promotes it to the top. -->
    <li v-for="(entry, i) in rows" :key="i" v-copy="{ source: entry, sink: clipboard.history }">
      {{ entry }}
    </li>
  </ul>
</template>

Everything else

Every option, modifier and event — dedupe scopes, .rich labels, the reactive controller, custom triggers, within-scoped selection copy, copy-result — is driven in a real browser on the v-copy playground tab, one card per feature, every card editable in place: bare binding · copy history · the history picker · aggregated multi-select copy · copy what the user selected · nothing to copy

CHANGELOG.md · ARCHITECTURE.md

License

MIT

Related Packages

vue3-copy

This is fork from egoist/v-copy and now support Vue 3.

1,158 1
@ozjsey/v-select-text

Vue 3 directive for reactive text selection — select the rendered text of any el...

11 0
v-clipboard

Vue.js Clipboard Plugin

1,101,981 515
@scalar/use-clipboard

tiny wrapper around the clipboard API

37,211 16,304
@meforma/vue-copy-to-clipboard

Vue.js 3 plugin to copy text to clipboard

16,239 12

Version History

Version Released Vue License
1.2.3 ^3.0.0 MIT
1.2.2 ^3.0.0 MIT
1.2.1 ^3.0.0 MIT
1.2.0 ^3.0.0 MIT
1.1.0 ^3.0.0 MIT