Copy to clipboard with stimulus using the Clipboard API

In this article I will describe, how to use the Clipboard API in a Hotwire Stimulus controller to copy content to your clipboard.

First, let me show you what this might look like

Copy to clipboard using the Clipboard API

Now let's get started and create a clipboard controller

rails generate stimulus clipboard

This will generate some boilerplate for the stimulus controller

// app/javascript/controllers/clipboard_controller.js
import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="clipboard"
export default class extends Controller {
  connect() {
  }
}

Required HTML markup

The HTML we'll use for this example looks like this

<div data-controller="clipboard">
  <span data-clipboard-target="source" data-clipboard-text="Content to copy">
    Content to copy
  </span>
  <button data-action="clipboard#copy" title="Copy to clipboard">
    Copy
  </button>
  <span data-clipboard-target="copiedIndicator" class="hidden text-green-700 font-semibold">
    copied
  </span>
</div>

I have deliberately removed most of the CSS classes as they may be different anyway and I want to focus on the structure. To initialize the clipboard controller you need to add data-controller="clipboard" to any wrapping element.

The example has the content to be copied in a data-clipboard-text attribute. I could have copied it from the span's innerHTML, but that would include the spacing, which I then need to take care of.

The button itself will triggers the copy to clipboard functionality. In the example, it is just text, but it could be a icon if you like. It triggers the the copy function by adding data-action="clipboard#copy" to it. There are a number of standard actions defined by stimulus. For example, you don't need to tell stimulus that it's a click action when using a button or a link.

Then there is a hidden span containing some indicator text to be displayed after the content has been successfully copied to the clipboard.

The copy function

To copy the content using the clipboard API, we'll use the writeText method on the clipboard object. Calling navigator.clipboard has already prepared one for us.

copy(event) {
  // prevent the default action of the trigger element
  event.preventDefault()

  // copy the content from the sourceTarget to the clipboard
  navigator.clipboard.writeText(this.sourceTarget.dataset.clipboardText);

  // show the copied indicator
  this.copiedIndicatorTarget.classList.remove("hidden");

  // hide the copied indicator after 2 seconds
  setTimeout(() => {
    this.copiedIndicatorTarget.classList.add("hidden");
  }, 2000)
}

This is straightforward. We take the content we want to copy from sourceTarget and put it on the clipboard. Then we show our indicator that we have copied the content. Then we hide it two seconds later. That's it.

Putting it all together

To complete the picture, this is the clipboard controller

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="clipboard"
export default class extends Controller {
  static targets = [
    "source",
    "copiedIndicator"
  ]

  connect() {
  }

  copy(event) {
    // prevent the default action of the trigger element
    event.preventDefault()

    // copy the content from the sourceTarget to the clipboard
    navigator.clipboard.writeText(this.sourceTarget.dataset.clipboardText);

    // show the copied indicator
    this.copiedIndicatorTarget.classList.remove("hidden");

    // hide the copied indicator after 2 seconds
    setTimeout(() => {
      this.copiedIndicatorTarget.classList.add("hidden");
    }, 2000)
  }
}

Copy from form inputs

You may be thinking, "Stefan, I have an input field to copy the content from. How does that work?". No problem. I got you covered.

Instead of using a span to copy the content from, we'll use an input field

<input data-clipboard-target="source" value="Content to copy">

and instead of using the data attribute to copy the content from, we'll use the value of the input field.

// copy the content from the sourceTarget to the clipboard
navigator.clipboard.writeText(this.sourceTarget.value);

That's it. Not so much, we have to adapt.

Wrapping it up

I hope you found this article useful and that you learned something new.

If you have any questions or feedback, didn't understand something, or found a mistake, please send me an email or drop me a note on twitter / x. I look forward to hearing from you.

Consider subscribing to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you 🙏