<div data-controller="clipboard" data-clipboard-text-value="<%= invite_url(@invite) %>">
<button type="button" data-action="clipboard#copy" class="rounded bg-gray-100 px-3 py-1">
Copy invite link
</button>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { text: String }
async copy(event) {
const btn = event.currentTarget
const previous = btn.textContent
try {
await navigator.clipboard.writeText(this.textValue)
btn.textContent = "Copied"
} catch {
// fallback: create a temporary input
const input = document.createElement("input")
input.value = this.textValue
document.body.appendChild(input)
input.select()
document.execCommand("copy")
input.remove()
btn.textContent = "Copied"
}
setTimeout(() => (btn.textContent = previous), 1200)
}
}
Copy buttons are everywhere (invite links, API keys, CLI commands). With Stimulus, I keep it tiny and resilient: use navigator.clipboard.writeText when available, and fall back to selecting a hidden input for older browsers. I also provide immediate feedback by swapping the button label to “Copied” for a second. This is a perfect example of where Hotwire shines: the baseline is simple server-rendered HTML, and a small controller adds polish without turning the page into a JS app. When I’m copying sensitive content, I keep it out of the DOM when possible and fetch it just-in-time, but for normal URLs this approach is straightforward.