UI Components Easy
Clipboard Copy
Copy-to-clipboard button with success feedback animation — icon changes to checkmark, reverts after 2s. Works on any text.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
/* ── Demo shell ── */
.demo {
width: min(560px, 100%);
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.demo-title {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.02em;
color: #f2f6ff;
}
.demo-sub {
font-size: 0.9rem;
color: #8090b0;
line-height: 1.6;
}
/* ── Snippet list ── */
.snippet-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* ── Snippet block ── */
.snippet-block {
border-radius: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.07);
background: rgba(255, 255, 255, 0.03);
overflow: hidden;
}
.snippet-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.55rem 0.9rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(255, 255, 255, 0.02);
}
.snippet-lang {
font-size: 0.72rem;
font-weight: 500;
color: #5a6a8a;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.snippet-pre {
padding: 1rem 1.1rem;
overflow-x: auto;
}
.snippet-pre code {
font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace;
font-size: 0.82rem;
line-height: 1.65;
color: #c8d8f8;
white-space: pre;
}
/* ── Copy button ── */
.copy-btn {
position: relative;
width: 1.9rem;
height: 1.9rem;
border-radius: 0.4rem;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
color: #7888a8;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
flex-shrink: 0;
}
.copy-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: #c5d3f0;
border-color: rgba(255, 255, 255, 0.14);
}
.copy-btn:active {
transform: scale(0.92);
}
.copy-btn:focus-visible {
outline: 2px solid #4f6ef7;
outline-offset: 2px;
}
/* Icon states */
.copy-btn__icon {
position: absolute;
transition: opacity 0.15s ease, transform 0.2s ease;
}
.copy-btn__icon--check {
opacity: 0;
transform: scale(0.7);
color: #4ade80;
}
/* Copied state */
.copy-btn.is-copied {
background: rgba(74, 222, 128, 0.1);
border-color: rgba(74, 222, 128, 0.3);
color: #4ade80;
}
.copy-btn.is-copied .copy-btn__icon--copy {
opacity: 0;
transform: scale(0.7);
}
.copy-btn.is-copied .copy-btn__icon--check {
opacity: 1;
transform: scale(1);
animation: check-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Error state */
.copy-btn.is-error {
background: rgba(248, 113, 113, 0.1);
border-color: rgba(248, 113, 113, 0.3);
color: #f87171;
animation: err-shake 0.4s ease;
}
@keyframes check-pop {
0% {
transform: scale(0.5);
}
60% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
@keyframes err-shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-3px);
}
75% {
transform: translateX(3px);
}
}(function () {
"use strict";
const RESET_DELAY = 2000;
/**
* Copy the text content of an element to the clipboard.
* @param {HTMLButtonElement} btn The copy button that was activated
*/
async function handleCopy(btn) {
const targetId = btn.dataset.copy;
const source = document.getElementById(targetId);
if (!source) return;
const text = source.textContent ?? "";
try {
await navigator.clipboard.writeText(text);
setButtonState(btn, "copied");
} catch {
setButtonState(btn, "error");
}
}
/**
* Apply a visual state to the button, then revert after RESET_DELAY ms.
* @param {HTMLButtonElement} btn
* @param {"copied"|"error"} state
*/
function setButtonState(btn, state) {
// Clear any pending reset
clearTimeout(btn._resetTimer);
// Remove both possible state classes first
btn.classList.remove("is-copied", "is-error");
if (state === "copied") {
btn.classList.add("is-copied");
btn.setAttribute("aria-label", "Copied!");
} else {
btn.classList.add("is-error");
btn.setAttribute("aria-label", "Failed to copy");
}
// Revert to default
btn._resetTimer = setTimeout(() => {
btn.classList.remove("is-copied", "is-error");
btn.setAttribute("aria-label", "Copy to clipboard");
}, RESET_DELAY);
}
// Event delegation — handles all copy buttons
document.addEventListener("click", (e) => {
const btn = e.target.closest(".copy-btn");
if (btn) handleCopy(btn);
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Clipboard Copy</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h2 class="demo-title">Clipboard Copy</h2>
<p class="demo-sub">Click the copy button on any snippet — the icon confirms success.</p>
<div class="snippet-list">
<!-- Snippet 1 -->
<div class="snippet-block">
<div class="snippet-header">
<span class="snippet-lang">bash</span>
<button
class="copy-btn"
data-copy="snippet-1"
aria-label="Copy to clipboard"
>
<svg class="copy-btn__icon copy-btn__icon--copy" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
<svg class="copy-btn__icon copy-btn__icon--check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="20 6 9 17 4 12"/>
</svg>
</button>
</div>
<pre class="snippet-pre"><code id="snippet-1">npm install @stealthis/ui</code></pre>
</div>
<!-- Snippet 2 -->
<div class="snippet-block">
<div class="snippet-header">
<span class="snippet-lang">javascript</span>
<button
class="copy-btn"
data-copy="snippet-2"
aria-label="Copy to clipboard"
>
<svg class="copy-btn__icon copy-btn__icon--copy" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
<svg class="copy-btn__icon copy-btn__icon--check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="20 6 9 17 4 12"/>
</svg>
</button>
</div>
<pre class="snippet-pre"><code id="snippet-2">import { copyToClipboard } from '@stealthis/ui';
copyToClipboard(text).then(() => console.log('Copied!'));</code></pre>
</div>
<!-- Snippet 3 -->
<div class="snippet-block">
<div class="snippet-header">
<span class="snippet-lang">env</span>
<button
class="copy-btn"
data-copy="snippet-3"
aria-label="Copy to clipboard"
>
<svg class="copy-btn__icon copy-btn__icon--copy" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
<svg class="copy-btn__icon copy-btn__icon--check" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="20 6 9 17 4 12"/>
</svg>
</button>
</div>
<pre class="snippet-pre"><code id="snippet-3">API_KEY=sk-stealthis-a1b2c3d4e5f6g7h8i9j0
PUBLIC_URL=https://stealthis.dev</code></pre>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Clipboard Copy
A reusable copy-to-clipboard button with polished success feedback. When clicked, the icon morphs into a checkmark with a brief scale animation, then reverts to the copy icon after 2 seconds — giving users clear confirmation without interrupting their flow.
How it works
- Each copy button holds a
data-copyattribute pointing to the ID of the<code>element to copy navigator.clipboard.writeText()writes the text asynchronously- On success, a
.is-copiedclass swaps the icon and triggers a CSS keyframe animation - A
setTimeoutresets the state after 2 seconds - Falls back gracefully if
navigator.clipboardis unavailable
States
- Default — copy icon, neutral background
- Hover — subtle highlight on the button
- Copied — checkmark icon, green accent, scale pulse
- Error — red flash if clipboard write fails
When to use it
- Code snippet blocks
- API key / token displays
- Shareable URL fields
- Any “one-click copy” interface