Interaction blueprint
Copy confirmation
The control confirms a silent clipboard action right where it happened.
Build it
Choose a target and adapt the source to your interface.
<div class="copy-demo">
<button class="copy-button" type="button" data-copied="false">
<span class="copy-icon" aria-hidden="true"
><svg class="copy-pages" viewBox="0 0 24 24">
<rect x="8" y="3" width="12" height="15" rx="2" />
<path d="M16 18v3H4V6h4" /></svg
><svg class="copy-check" viewBox="0 0 24 24">
<path d="m5 12 4 4L19 6" /></svg
></span>
Copy sample text
</button>
<p class="copy-status" role="status"></p>
</div>
<script>
const button = document.querySelector('.copy-button');
const status = document.querySelector('.copy-status');
let timer;
button.addEventListener('click', async () => {
button.disabled = true;
clearTimeout(timer);
try {
await navigator.clipboard.writeText('Sample text');
button.dataset.copied = 'true';
status.textContent = 'Copied to clipboard.';
timer = setTimeout(() => {
button.dataset.copied = 'false';
status.textContent = '';
}, 1800);
} catch {
button.dataset.copied = 'false';
status.textContent = 'Copy failed. Check browser clipboard permission.';
} finally {
button.disabled = false;
}
});
</script>
<style>
.copy-demo {
display: grid;
justify-items: center;
width: 280px;
max-width: 100%;
gap: 10px;
}
.copy-status {
box-sizing: border-box;
width: 100%;
min-height: 40px;
margin: 0;
text-align: center;
font: 12px/20px system-ui;
color: var(--stage-muted, #68778c);
}
.copy-button {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 10px 16px;
border: 1px solid #68778c;
border-radius: 6px;
background: white;
color: #20252d;
font: 14px system-ui;
cursor: pointer;
}
.copy-icon {
display: inline-grid;
width: 24px;
height: 24px;
place-items: center;
}
.copy-icon svg {
grid-area: 1 / 1;
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
transition:
transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 260ms ease-out;
}
.copy-check {
opacity: 0;
transform: translateY(4px);
}
.copy-button[data-copied='true'] .copy-pages {
opacity: 0;
transform: translateY(-4px);
}
.copy-button[data-copied='true'] .copy-check {
opacity: 1;
transform: none;
}
.copy-button:focus-visible {
outline: 2px solid #175bc5;
outline-offset: 4px;
}
@media (prefers-reduced-motion: reduce) {
.copy-icon svg {
transition: none;
}
}
</style>Animation breakdown
Frame phases, motion intent, implementation constraints and accessible alternatives.
01
Rest: overlapping document outline.
02
0–100ms: top page lifts 4px.
03
100–220ms: checkmark replaces copy glyph.
04
220–260ms: hold before reset.
Motion intent
The control confirms a silent clipboard action right where it happened.
This duration leaves enough time to read the state change while keeping the interface responsive.
The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.
Motion specification
- Trigger
- The copy action succeeds.
- Timing
- 260ms
- Easing
- cubic-bezier(.22, 1, .36, 1)
Animated properties
transformopacity
Implementation notes
- Keep copy and copied aligned to the same visual anchor.
- Drive the transition from semantic state, not a timer, when the trigger is interactive.
- Use transform, opacity and avoid layout work inside the animation loop.
Reduced motion
Swap to the final state instantly, preserving colour, label, and accessible state.
- Use a minimum 44 by 44 pixel interactive target.
- Expose the copy and copied states with aria-pressed, aria-expanded, or an updated accessible label.
- Keep visible keyboard focus independent from the animation.
Avoid
- Do not add bounce unless a real object pickup or spring settle is part of the interaction.
- Do not hide the final state from assistive technology.
- Do not make the user wait for animation before the underlying state updates.