Back to the library
Wax-resist save seal, held hold
Variant: Held hold
Fast in, long hold: the dots and ring land on a compressed 40ms cascade, then the finished seal sits legible for most of the cycle before a quick fade. Use it where the confirmation must survive a glance that arrives late — a save fired from a toolbar the user is not currently looking at. The enter is the shortest in the family and the exit is shorter still, so the only thing lengthened is the dwell.
confirmationsavedwellsvgseal
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>Wax-resist save seal — held hold</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: 5.4s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--_dot: calc(120ms / var(--speed));
--_step: calc(40ms / var(--speed));
--_ring: calc(220ms / var(--speed));
--_check: calc(180ms / var(--speed));
--_label: calc(150ms / var(--speed));
--_lead: calc(80ms / var(--speed));
}
* { box-sizing: border-box; }
body {
margin: 0;
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);
}
.card {
width: min(320px, 88vw);
padding: clamp(18px, 5vw, 26px);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
display: grid;
justify-items: center;
gap: clamp(12px, 3.5vw, 16px);
}
.seal {
width: clamp(84px, 26vw, 104px);
aspect-ratio: 1;
display: block;
overflow: visible;
}
.plate { fill: var(--ivory); stroke: var(--oat); stroke-width: 1; }
.ring {
fill: none;
stroke: var(--clay);
stroke-width: 1.6;
stroke-linecap: round;
stroke-dasharray: 213.6;
stroke-dashoffset: 213.6;
transform: rotate(-90deg);
transform-origin: 50% 50%;
animation: ring-close var(--t) var(--ease-out) infinite;
}
.dot {
fill: var(--clay-d);
opacity: 0;
transform: scale(0.94);
transform-origin: 50% 50%;
animation: dot-lay var(--t) var(--ease-out) infinite;
}
/* 40ms apart is below the threshold where a cascade reads as sequence;
the dots effectively co-arrive, which is the point of this tuning. */
.dot:nth-child(1) { animation-delay: calc(var(--_lead)); }
.dot:nth-child(2) { animation-delay: calc(var(--_lead) + var(--_step)); }
.dot:nth-child(3) { animation-delay: calc(var(--_lead) + 2 * var(--_step)); }
.dot:nth-child(4) { animation-delay: calc(var(--_lead) + 3 * var(--_step)); }
.tick {
fill: none;
stroke: var(--olive);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 30;
stroke-dashoffset: 30;
animation: tick-draw var(--t) var(--ease-out) infinite;
animation-delay: calc(var(--_lead) + 4 * var(--_step));
}
.label {
font-size: clamp(11px, 3.2vw, 13px);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--clay-d);
opacity: 0;
animation: label-in var(--t) var(--ease-out) infinite;
animation-delay: calc(var(--_lead) + 5 * var(--_step));
}
/* Enter occupies the first ~5% of the cycle; the dwell owns the rest. */
@keyframes ring-close {
0% { stroke-dashoffset: 213.6; }
4% { stroke-dashoffset: 0; }
92% { stroke-dashoffset: 0; opacity: 1; }
97% { stroke-dashoffset: 0; opacity: 0; }
100% { stroke-dashoffset: 213.6; opacity: 0; }
}
@keyframes dot-lay {
0% { opacity: 0; transform: scale(0.94); }
2.5% { opacity: 1; transform: scale(1); }
90% { opacity: 1; transform: scale(1); }
95% { opacity: 0; transform: scale(0.97); }
100% { opacity: 0; transform: scale(0.94); }
}
@keyframes tick-draw {
0% { stroke-dashoffset: 30; }
3.5% { stroke-dashoffset: 0; }
89% { stroke-dashoffset: 0; opacity: 1; }
94% { stroke-dashoffset: 0; opacity: 0; }
100% { stroke-dashoffset: 30; opacity: 0; }
}
@keyframes label-in {
0% { opacity: 0; transform: translateY(3px); }
3% { opacity: 1; transform: translateY(0); }
89% { opacity: 1; transform: translateY(0); }
94% { opacity: 0; transform: translateY(0); }
100% { opacity: 0; transform: translateY(3px); }
}
@media (max-width: 380px) {
.card { padding: 16px; gap: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.ring, .tick { stroke-dashoffset: 0; }
.ring {
animation: seal-breathe calc(var(--cycle) * 1.5 / var(--speed)) ease-in-out infinite;
}
.dot, .tick, .label {
animation: seal-breathe calc(var(--cycle) * 1.5 / var(--speed)) ease-in-out infinite;
opacity: 1;
transform: none;
}
.dot:nth-child(1), .dot:nth-child(2),
.dot:nth-child(3), .dot:nth-child(4),
.tick, .label { animation-delay: 0s; }
@keyframes seal-breathe {
0%, 100% { opacity: 0.62; }
50% { opacity: 1; }
}
}
</style>
</head>
<body>
<div class="card">
<svg class="seal" viewBox="0 0 100 100" role="img" aria-label="Saved">
<circle class="plate" cx="50" cy="50" r="42" />
<circle class="ring" cx="50" cy="50" r="34" />
<g>
<circle class="dot" cx="50" cy="27" r="3.2" />
<circle class="dot" cx="73" cy="50" r="3.2" />
<circle class="dot" cx="50" cy="73" r="3.2" />
<circle class="dot" cx="27" cy="50" r="3.2" />
</g>
<path class="tick" d="M38 51 L47 60 L63 42" />
</svg>
<span class="label">Saved</span>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.