A command box whose copy button flips to a check and 'Copied' on click, holds for a moment, then resets by itself. The pressed state snaps in and the rest state returns on a delayed transition.
Also known as: copy button css, copied button animation, copy code button.
$ npm i @acme/ui
<div class="btn-copy-clipboard">
<code class="btn-copy-clipboard-code"><span class="btn-copy-clipboard-prompt">$</span> npm i @acme/ui</code>
<button class="btn-copy-clipboard-btn" type="button" aria-label="Copy to clipboard">
<span class="btn-copy-clipboard-icons" aria-hidden="true">
<svg viewBox="0 0 24 24"><rect x="8.5" y="8.5" width="11" height="11" rx="2.5"/><path d="M15.5 8.5V6.5A2 2 0 0 0 13.5 4.5H6.5A2 2 0 0 0 4.5 6.5v7a2 2 0 0 0 2 2h2"/></svg>
<svg viewBox="0 0 24 24"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>
</span>
<span class="btn-copy-clipboard-text" aria-hidden="true"><span>Copy</span><span>Copied</span></span>
</button>
</div>.btn-copy-clipboard {
--c: var(--btn-color, #3b82f6);
font-size: var(--btn-size, 18px);
display: inline-flex;
align-items: center;
gap: 1.1em;
padding: 0.4em 0.4em 0.4em 1.1em;
border-radius: 0.75em;
background: color-mix(in srgb, var(--c) 12%, #0f172a);
color: #e2e8f0;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 0.5em 1.2em -0.6em rgba(2, 6, 23, 0.6);
}
.btn-copy-clipboard-code {
font: 500 0.9em/1 ui-monospace, SFMono-Regular, Menlo, monospace;
white-space: nowrap;
}
.btn-copy-clipboard-prompt { color: color-mix(in srgb, var(--c) 60%, #fff); }
.btn-copy-clipboard-btn {
display: inline-flex;
align-items: center;
gap: 0.45em;
padding: 0.6em 0.85em;
border: 0;
border-radius: 0.5em;
background-color: rgba(255, 255, 255, 0.09);
color: #fff;
font: 600 0.85em/1 system-ui, sans-serif;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: background-color 0.3s ease 1.2s, transform 0.25s cubic-bezier(0.3, 0.7, 0.4, 1.5);
}
.btn-copy-clipboard-icons {
position: relative;
width: 1.15em;
height: 1.15em;
}
.btn-copy-clipboard-icons svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.3s ease 1.2s, opacity 0.3s ease 1.2s;
}
.btn-copy-clipboard-icons svg:last-child {
stroke-width: 3;
opacity: 0;
transform: scale(0.3) rotate(-30deg);
}
.btn-copy-clipboard-text {
display: grid;
overflow: hidden;
}
.btn-copy-clipboard-text span {
grid-area: 1 / 1;
transition: transform 0.3s cubic-bezier(0.3, 0.7, 0.4, 1) 1.2s, opacity 0.3s ease 1.2s;
}
.btn-copy-clipboard-text span:last-child {
opacity: 0;
transform: translateY(100%);
}
.btn-copy-clipboard-btn:focus-visible {
outline: 2px solid var(--c);
outline-offset: 0.15em;
}
.btn-copy-clipboard-btn:active {
background-color: var(--c);
transform: scale(0.94);
transition-delay: 0s;
}
.btn-copy-clipboard-btn:active .btn-copy-clipboard-icons svg {
transition: transform 0.35s cubic-bezier(0.3, 0.7, 0.4, 1.5), opacity 0.15s ease;
}
.btn-copy-clipboard-btn:active .btn-copy-clipboard-icons svg:first-child {
opacity: 0;
transform: scale(0.3) rotate(30deg);
}
.btn-copy-clipboard-btn:active .btn-copy-clipboard-icons svg:last-child {
opacity: 1;
transform: none;
}
.btn-copy-clipboard-btn:active .btn-copy-clipboard-text span {
transition: transform 0.3s cubic-bezier(0.3, 0.7, 0.4, 1), opacity 0.2s ease;
}
.btn-copy-clipboard-btn:active .btn-copy-clipboard-text span:first-child {
opacity: 0;
transform: translateY(-100%);
}
.btn-copy-clipboard-btn:active .btn-copy-clipboard-text span:last-child {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.btn-copy-clipboard, .btn-copy-clipboard *, .btn-copy-clipboard::before, .btn-copy-clipboard::after, .btn-copy-clipboard *::before, .btn-copy-clipboard *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}The same effect using Tailwind utility classes. No custom CSS or config needed.
<div class="inline-flex items-center gap-4 rounded-xl bg-slate-900 py-1.5 pl-4 pr-1.5 text-slate-200 ring-1 ring-inset ring-white/10">
<code class="whitespace-nowrap font-mono text-sm"><span class="text-blue-400">$</span> npm i @acme/ui</code>
<button class="group flex items-center gap-2 rounded-lg bg-white/10 px-3 py-2 text-xs font-semibold text-white [transition:background-color_.3s_ease_1.2s,transform_.25s] active:scale-95 active:bg-blue-500 active:[transition-delay:0s]" type="button" aria-label="Copy to clipboard">
<span class="relative h-4 w-4" aria-hidden="true">
<svg class="absolute inset-0 h-4 w-4 fill-none stroke-current stroke-2 transition duration-300 delay-[1200ms] group-active:scale-0 group-active:opacity-0 group-active:delay-0" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="8.5" width="11" height="11" rx="2.5"/><path d="M15.5 8.5V6.5A2 2 0 0 0 13.5 4.5H6.5A2 2 0 0 0 4.5 6.5v7a2 2 0 0 0 2 2h2"/></svg>
<svg class="absolute inset-0 h-4 w-4 scale-0 fill-none stroke-current stroke-[3] opacity-0 transition duration-300 delay-[1200ms] group-active:scale-100 group-active:opacity-100 group-active:delay-0" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>
</span>
<span class="grid overflow-hidden" aria-hidden="true">
<span class="col-start-1 row-start-1 transition duration-300 delay-[1200ms] group-active:-translate-y-full group-active:opacity-0 group-active:delay-0">Copy</span>
<span class="col-start-1 row-start-1 translate-y-full opacity-0 transition duration-300 delay-[1200ms] group-active:translate-y-0 group-active:opacity-100 group-active:delay-0">Copied</span>
</span>
</button>
</div>--btn-color on the element or a parent.
Currently previewing #3b82f6.--btn-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 18px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.