Edit lease — heartbeat
Variant: Heartbeat
The healthy case for any soft lock — a file someone else is editing, a job a worker has claimed, a seat on hold. The lease drains linearly, because it is a clock, toward a mark at one third remaining, and is renewed the moment it gets there: a 190ms ease-out refill against 2.1s of drain, fast enough that renewing never looks like effort. The mark is the whole design decision. A client that renews at the last second is one dropped packet from losing the lock, so the margin is drawn where everyone can see it, and the bar never once gets below it. A single ring leaves the heartbeat dot on each renewal and is gone long before the next, so the dot reads as a pulse you can count rather than a light that is merely on. Reach for it beside any "Mara is editing" chip where viewers need to know the claim is alive, not just asserted.
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>Edit lease — heartbeat</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)
);
--_muted: color-mix(in srgb, var(--slate) 50%, transparent);
--_hair: color-mix(in srgb, var(--slate) 9%, transparent);
/* Clock — one renewal period: 2.1s of linear drain down to the mark,
a 190ms refill, and a beat of rest. The lease is 30s and renews with
10s left; the demo keeps that ratio and compresses the seconds. */
--cycle: 2.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);
}
.card {
width: min(300px, calc(100vw - 32px));
padding: clamp(14px, 4.5vw, 18px);
border-radius: calc(14px * var(--round));
background: var(--paper);
box-shadow:
0 0 0 1px var(--_hair),
0 1px 2px var(--_hair);
}
.doc {
display: flex;
align-items: center;
gap: 7px;
padding-bottom: 12px;
margin-bottom: 12px;
border-bottom: 1px solid var(--_hair);
font-size: 12.5px;
font-weight: 500;
letter-spacing: -0.005em;
}
.doc svg {
flex: none;
width: 14px;
height: 14px;
fill: none;
stroke: var(--_muted);
stroke-width: 1.4;
stroke-linejoin: round;
}
.hold {
display: flex;
align-items: center;
gap: 8px;
height: 32px;
}
.lock {
flex: none;
width: 16px;
height: 16px;
fill: none;
stroke: color-mix(in srgb, var(--slate) 72%, transparent);
stroke-width: 1.6;
stroke-linecap: round;
}
.lock rect {
fill: color-mix(in srgb, var(--slate) 72%, transparent);
stroke: none;
}
.avatar {
flex: none;
display: grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: color-mix(in srgb, var(--olive) 18%, var(--paper));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--olive) 34%, transparent);
font-size: 11px;
font-weight: 600;
color: color-mix(in srgb, var(--olive) 70%, var(--slate));
}
.who {
flex: 1;
min-width: 0;
font-size: 13px;
font-weight: 500;
letter-spacing: -0.01em;
}
.beat {
position: relative;
flex: none;
width: 8px;
height: 8px;
margin-right: 2px;
border-radius: 50%;
background: var(--olive);
}
/* The ring marks the renewal, not the state: it leaves once per beat and
is gone long before the next one, so the dot reads as a pulse you can
count rather than a light that is merely on. */
.beat::after {
content: '';
position: absolute;
inset: 0;
border-radius: 50%;
border: 1.5px solid var(--olive);
opacity: 0;
animation: ping var(--t) var(--ease-out) infinite;
}
.lease {
position: relative;
height: 4px;
margin-top: 8px;
border-radius: 999px;
background: var(--oat);
}
.fill {
position: absolute;
inset: 0;
border-radius: 999px;
background: var(--olive);
transform-origin: left center;
animation: drain var(--t) infinite;
}
/* Renew at a third remaining — the margin is the point. A client that
waits for the last second is one dropped packet from losing the lock. */
.mark {
position: absolute;
left: 33.333%;
top: -4px;
width: 1.5px;
height: 12px;
margin-left: -0.75px;
border-radius: 1px;
background: color-mix(in srgb, var(--slate) 38%, transparent);
}
.foot {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 11.5px;
color: var(--_muted);
font-variant-numeric: tabular-nums;
}
/* The drain is a clock, so it is linear. The refill is the server saying
yes, so it decelerates — and it is fast, 190ms against 2.1s, because a
renewal that took visible effort would read as the lease being at risk. */
@keyframes drain {
0% {
transform: scaleX(1);
animation-timing-function: linear;
}
87.5% {
transform: scaleX(0.3333);
animation-timing-function: var(--ease-out);
}
95.4%,
100% {
transform: scaleX(1);
}
}
@keyframes ping {
0%,
87.4% {
opacity: 0;
transform: scale(1);
}
87.5% {
opacity: 0.6;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(2.6);
}
}
@media (prefers-reduced-motion: reduce) {
/* The bar is the datum and stays, slowed. The ring stops travelling and
becomes a soft glow on the same beat. */
.fill {
animation-duration: calc(var(--t) * 1.75);
}
.beat::after {
animation-name: glow;
animation-duration: calc(var(--t) * 1.75);
}
@keyframes glow {
0%,
87.4% {
opacity: 0;
transform: scale(1.8);
}
87.5% {
opacity: 0.45;
transform: scale(1.8);
}
100% {
opacity: 0;
transform: scale(1.8);
}
}
}
</style>
</head>
<body>
<div
class="card"
role="img"
aria-label="A document locked by Mara. Her edit lease drains toward a mark at one third remaining and is renewed the moment it reaches it, so the bar never runs out and a heartbeat dot pings on each renewal"
>
<div class="doc">
<svg viewBox="0 0 14 14" aria-hidden="true">
<path d="M3 1.5h5.2L11 4.3v8.2H3z M8 1.5v3h3" />
</svg>
<span>pricing-q4.md</span>
</div>
<div class="hold">
<svg class="lock" viewBox="0 0 16 16" aria-hidden="true">
<path d="M5 7.5V5.2a3 3 0 0 1 6 0v2.3" />
<rect x="3" y="7" width="10" height="7.5" rx="1.8" />
</svg>
<span class="avatar">MO</span>
<span class="who">Mara is editing</span>
<span class="beat"></span>
</div>
<div class="lease"><i class="fill"></i><b class="mark"></b></div>
<div class="foot"><span>Lease 30s</span><span>renews at 10s</span></div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.