Back to the library
Hinge reveal save, bold
Variant: Bold
The heavy end of the hinge reveal. A larger tile, doors that fold back 135° toward the viewer, a thick check on a solid field, and a longer rise on the status words. Use it for infrequent saves that matter, such as publishing or submitting a form, where the user should feel that the action is finished. The stage has extra side padding sized to the projected swing, so the open doors never cover the text or leave the card at phone widths.
confirmationsavehingeemphaticstate-change
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>Hinge reveal save — Bold</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; /* include ONLY if at least one border-radius is wrapped below */
--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.2s; /* the authored master loop */
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* include only if used */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* include only if used */
--ease-accent: cubic-bezier(0.32, 0.72, 0, 1); /* the snippet's ONE characterful curve, if it has one */
/* Weight — the only values that differ between siblings.
--_reach covers the projected swing: at 135deg the far edge lands ~28px past the tile. */
--_tile: 64px;
--_open: 135deg;
--_reach: 26px;
--_stroke: 2.1;
--_seam: 2px;
--_field-o: 1;
--_ink: var(--paper);
--_shade: 0.6;
--_rise: 0.7em;
--_weight: 700;
}
* { 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(340px, 92vw);
display: flex;
align-items: center;
gap: clamp(6px, 2vw, 12px);
padding: clamp(12px, 3.5vw, 18px);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
}
.stage {
flex: none;
display: grid;
place-items: center;
padding: 4px var(--_reach);
}
.tile {
position: relative;
width: var(--_tile);
height: var(--_tile);
perspective: calc(var(--_tile) * 4);
}
.field {
position: absolute;
inset: 0;
border-radius: calc(7px * var(--round));
background: var(--clay);
opacity: var(--_field-o);
}
.check {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
fill: none;
stroke: var(--_ink);
stroke-width: var(--_stroke);
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 1;
stroke-dashoffset: 1;
opacity: 0;
animation: draw var(--t) linear infinite;
}
.door {
position: absolute;
top: 0;
bottom: 0;
width: 50%;
background: var(--paper);
border: var(--_seam) solid var(--oat);
animation: hinge var(--t) linear infinite;
}
/* The inner face darkens as it turns away from the light, so the fold reads as a physical plane. */
.door::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: var(--oat);
opacity: 0;
animation: shade var(--t) linear infinite;
}
.door-l {
--_dir: -1;
left: 0;
transform-origin: left center;
border-radius: calc(7px * var(--round)) 0 0 calc(7px * var(--round));
}
.door-r {
--_dir: 1;
right: 0;
transform-origin: right center;
border-radius: 0 calc(7px * var(--round)) calc(7px * var(--round)) 0;
}
.copy { min-width: 0; }
.title {
margin: 0 0 3px;
font-size: clamp(14px, 3.9vw, 16px);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status {
margin: 0;
font-size: clamp(12px, 3.4vw, 13px);
white-space: nowrap;
}
.word {
display: inline-block;
animation: rise var(--t) linear infinite both;
}
.word:first-child {
color: var(--clay-d);
font-weight: var(--_weight);
margin-right: 0.3em;
}
.word + .word {
color: color-mix(in oklch, var(--slate) 55%, var(--paper));
animation-delay: calc(60ms / var(--speed));
}
/* 8→16% of 3.2s = 256ms open; 76→82% = 192ms close. Exit beats enter. */
@keyframes hinge {
0%, 8% { transform: rotateY(0deg); animation-timing-function: var(--ease-accent); }
16%, 76% { transform: rotateY(calc(var(--_open) * var(--_dir))); animation-timing-function: var(--ease-in-out); }
82%, 100% { transform: rotateY(0deg); }
}
@keyframes shade {
0%, 8% { opacity: 0; animation-timing-function: var(--ease-accent); }
16%, 76% { opacity: var(--_shade); animation-timing-function: var(--ease-in-out); }
82%, 100% { opacity: 0; }
}
/* The check starts drawing while the doors are half open, so it reads as one reveal rather than two steps. */
@keyframes draw {
0%, 12% { opacity: 0; stroke-dashoffset: 1; }
12.5% { opacity: 1; stroke-dashoffset: 1; animation-timing-function: var(--ease-out); }
21%, 73% { opacity: 1; stroke-dashoffset: 0; animation-timing-function: var(--ease-out); }
77%, 100% { opacity: 0; stroke-dashoffset: 0; }
}
@keyframes rise {
0%, 14% { opacity: 0; transform: translateY(var(--_rise)); animation-timing-function: var(--ease-out); }
22%, 72% { opacity: 1; transform: translateY(0); animation-timing-function: var(--ease-out); }
76%, 100% { opacity: 0; transform: translateY(0); }
}
@keyframes door-fade {
0%, 8% { opacity: 1; animation-timing-function: var(--ease-out); }
16%, 76% { opacity: 0; animation-timing-function: var(--ease-out); }
82%, 100% { opacity: 1; }
}
@keyframes fade {
0%, 14% { opacity: 0; animation-timing-function: var(--ease-out); }
22%, 72% { opacity: 1; animation-timing-function: var(--ease-out); }
76%, 100% { opacity: 0; }
}
@media (max-width: 380px) {
.card { padding: 12px; gap: 6px; }
.title { font-size: 14px; }
}
@media (prefers-reduced-motion: reduce) {
.door { transform: none; animation: door-fade calc(var(--t) * 2) linear infinite; }
.door::after { animation: none; opacity: 0; }
.check { stroke-dashoffset: 0; animation: fade calc(var(--t) * 2) linear infinite; }
.word { transform: none; animation: fade calc(var(--t) * 2) linear infinite both; }
.word + .word { animation-delay: calc(60ms / var(--speed)); }
}
</style>
</head>
<body>
<div class="card">
<div class="stage" aria-hidden="true">
<div class="tile">
<div class="field"></div>
<svg class="check" viewBox="0 0 24 24"><path pathLength="1" d="M6.5 12.5 L10.5 16.5 L17.5 8.5" /></svg>
<span class="door door-l"></span>
<span class="door door-r"></span>
</div>
</div>
<div class="copy">
<p class="title">Quarterly notes</p>
<p class="status"><span class="word">Saved</span><span class="word">just now</span></p>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.