Back to the library
Bleed fill confirm
Variant: Linear
The same press-to-confirm flood, but the clip-path circle advances at a constant rate for the full 260ms. Useful when the fill doubles as a progress readout — a linear edge lets a user judge how far along the commit is, which an ease-out curve actively hides. It reads more mechanical and slightly slower even at identical duration, because the first 100ms cover less ground.
buttonconfirmationclip-pathfeedbacklinear
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>Bleed fill confirm — linear</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
(#d97757 / #b85c3e / #788c5d / #b04a3f). */
--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: 3.6s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--_press: calc(140ms / var(--speed));
--_wipe: calc(260ms / var(--speed));
--_check: calc(240ms / var(--speed));
}
* { box-sizing: border-box; }
html, body { margin: 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);
-webkit-font-smoothing: antialiased;
}
.stage {
width: min(340px, 88vw);
display: grid;
justify-items: center;
gap: clamp(14px, 4vw, 20px);
padding: clamp(12px, 4vw, 20px);
}
.rule {
width: 100%;
height: 1px;
background: var(--oat);
}
.btn {
position: relative;
isolation: isolate;
width: min(230px, 76vw);
height: clamp(46px, 13vw, 52px);
border-radius: calc(10px * var(--round));
background: var(--paper);
border: 1px solid var(--oat);
overflow: hidden;
display: grid;
place-items: center;
transform-origin: 50% 50%;
animation: press var(--t) var(--ease-out) infinite;
}
.fill {
position: absolute;
inset: 0;
background: var(--clay-d);
clip-path: circle(0% at 26% 62%);
animation: flood var(--t) linear infinite;
}
.halo {
position: absolute;
inset: 0;
background: var(--clay);
clip-path: circle(0% at 26% 62%);
animation: halo var(--t) linear infinite;
}
.label {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 8px;
font-size: clamp(13px, 3.6vw, 14px);
font-weight: 560;
letter-spacing: 0.01em;
}
.word {
position: absolute;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
}
.word.a {
color: var(--slate);
animation: wordOut var(--t) var(--ease-out) infinite;
}
.word.b {
color: var(--paper);
padding-left: 24px;
opacity: 0;
animation: wordIn var(--t) var(--ease-out) infinite;
}
.tick {
position: absolute;
left: 50%;
z-index: 2;
width: 16px;
height: 16px;
margin-left: -58px;
opacity: 0;
animation: tickShow var(--t) var(--ease-out) infinite;
}
.tick path {
fill: none;
stroke: var(--paper);
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 22;
stroke-dashoffset: 22;
/* Constant-rate draw to match the fill's character. */
animation: draw var(--t) linear infinite;
}
.caption {
font-size: clamp(10.5px, 2.9vw, 11.5px);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--slate);
opacity: 0.45;
}
@keyframes press {
0%, 2% { transform: scale(1); }
4.5% { transform: scale(0.978); }
11% { transform: scale(1); }
100% { transform: scale(1); }
}
@keyframes flood {
0%, 4% { clip-path: circle(0% at 26% 62%); }
11.5% { clip-path: circle(130% at 26% 62%); }
88% { clip-path: circle(130% at 26% 62%); }
96%, 100% { clip-path: circle(0% at 26% 62%); }
}
@keyframes halo {
0%, 3.4% { clip-path: circle(0% at 26% 62%); }
10.4% { clip-path: circle(130% at 26% 62%); }
88% { clip-path: circle(130% at 26% 62%); }
95.4%, 100% { clip-path: circle(0% at 26% 62%); }
}
@keyframes wordOut {
0%, 3% { opacity: 1; transform: translateX(-50%) translateY(0); }
8% { opacity: 0; transform: translateX(-50%) translateY(-6px); }
90% { opacity: 0; transform: translateX(-50%) translateY(-6px); }
97%, 100% { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes wordIn {
0%, 7% { opacity: 0; transform: translateX(-50%) translateY(6px); }
14% { opacity: 1; transform: translateX(-50%) translateY(0); }
88% { opacity: 1; transform: translateX(-50%) translateY(0); }
93%, 100% { opacity: 0; transform: translateX(-50%) translateY(6px); }
}
@keyframes tickShow {
0%, 9% { opacity: 0; }
13% { opacity: 1; }
88% { opacity: 1; }
92%, 100% { opacity: 0; }
}
@keyframes draw {
0%, 10% { stroke-dashoffset: 22; }
20% { stroke-dashoffset: 0; }
88% { stroke-dashoffset: 0; }
92%, 100% { stroke-dashoffset: 22; }
}
@media (max-width: 380px) {
.stage { gap: 12px; padding: 10px; }
}
@media (prefers-reduced-motion: reduce) {
:root { --t: calc(6.8s / var(--speed)); }
.btn { animation: none; }
.fill {
clip-path: circle(130% at 26% 62%);
animation: fadeFill var(--t) ease infinite;
}
.halo { display: none; }
.word.a { animation: softOut var(--t) ease infinite; }
.word.b { animation: softIn var(--t) ease infinite; }
.tick { animation: tickShow var(--t) ease infinite; }
.tick path { stroke-dashoffset: 0; animation: none; }
@keyframes fadeFill {
0%, 8% { opacity: 0; }
22%, 88% { opacity: 1; }
98%, 100% { opacity: 0; }
}
@keyframes softOut {
0%, 6% { opacity: 1; transform: translateX(-50%); }
20%, 90% { opacity: 0; transform: translateX(-50%); }
100% { opacity: 1; transform: translateX(-50%); }
}
@keyframes softIn {
0%, 10% { opacity: 0; transform: translateX(-50%); }
24%, 88% { opacity: 1; transform: translateX(-50%); }
98%, 100% { opacity: 0; transform: translateX(-50%); }
}
}
</style>
</head>
<body>
<div class="stage">
<div class="rule"></div>
<div class="btn">
<div class="fill"></div>
<div class="halo"></div>
<svg class="tick" viewBox="0 0 16 16" aria-hidden="true">
<path d="M3 8.4 L6.4 11.8 L13 4.6" />
</svg>
<div class="label">
<span class="word a">Save changes</span>
<span class="word b">Saved</span>
</div>
</div>
<div class="caption">Commit state</div>
<div class="rule"></div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.