Back to the library
Copy confirmation, wipe fill
Variant: Wipe fill
A clay panel wipes across the button in 220ms and leaves off the right edge rather than retreating the way it came. Because the confirmed face is a clipped duplicate of the first, background and label change colour on the same frame — separately timed colour transitions never stay that aligned. The loudest of the three; use it where copying is the point of the page.
successcontrolclip-path
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Copy confirmation — wipe fill</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--round: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex. */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(
calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
);
--rust: oklch(
calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
);
/* Clock — every duration in the document runs off --t. */
--cycle: 4s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.copy {
position: relative;
display: inline-grid;
height: 44px;
border: 1px solid var(--oat);
border-radius: calc(10px * var(--round));
background: var(--paper);
font: inherit;
font-size: 14px;
font-weight: 500;
color: var(--slate);
cursor: pointer;
overflow: hidden;
animation: press var(--t) infinite;
}
/* Both faces sit in the same grid cell and carry the padding themselves,
so the button is already as wide as the longer state and the clay
panel reaches the edges without being taken out of flow. */
.face {
grid-area: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 18px;
white-space: nowrap;
}
.face svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* The confirmed state is a second, complete copy of the button face laid
over the first. Clipping it is the only thing that animates, so the
label and its background change colour on exactly the same frame —
a pair of colour transitions can never stay that perfectly in step. */
.face.done {
background: var(--clay);
color: var(--ivory);
clip-path: inset(0 100% 0 0);
animation: wipe var(--t) infinite;
}
/* In 220ms, and it leaves off the right edge rather than retreating to
the left: the fill keeps travelling the way it came in, so the button
reads as passing through a state instead of undoing one. */
@keyframes wipe {
0%,
12% {
clip-path: inset(0 100% 0 0);
animation-timing-function: var(--ease-out);
}
17.5%,
68% {
clip-path: inset(0 0 0 0);
animation-timing-function: var(--ease-out);
}
72% {
clip-path: inset(0 0 0 100%);
}
72.5%,
100% {
clip-path: inset(0 100% 0 0);
}
}
@keyframes press {
0%,
10% {
transform: scale(1);
animation-timing-function: var(--ease-out);
}
12.5% {
transform: scale(0.97);
animation-timing-function: var(--ease-out);
}
16%,
100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.copy {
animation: none;
}
/* No travelling edge — the panel is present the whole time and only
its opacity changes, which keeps the colour swap doing the talking. */
.face.done {
clip-path: none;
animation: wipe-flat var(--t) ease infinite;
}
@keyframes wipe-flat {
0%,
12% {
opacity: 0;
}
17.5%,
68% {
opacity: 1;
}
72%,
100% {
opacity: 0;
}
}
}
</style>
</head>
<body>
<button class="copy" type="button">
<span class="face idle">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="5" y="5" width="14" height="16" rx="2.5" />
<rect x="9" y="2" width="6" height="4" rx="1.5" />
</svg>
Copy
</span>
<span class="face done" aria-hidden="true">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 12.5 10 17.5 19 7" />
</svg>
Copied
</span>
</button>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.