Countdown — threshold drain
Variant: Threshold drain
The zoned drain: a window emptying at one linear rate past two marks printed on the track at one half and one fifth. The fill snaps olive to clay to rust on the frames it crosses them — blending would claim states between the zones that the rules never defined — and the marks stay standing after the level has passed, so how far gone things are is always read against the line itself. A status dot holds still through two zones and gains a 512ms pulse only in the last: cadence carries the escalation to peripheral vision, where colour alone cannot. The 640ms hold at empty is deliberate — zero is a state, not a frame. Use it for rate limits, quota windows, and any meter governed by published thresholds, where the question is not how much is left but which rule applies right now.
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>Countdown — threshold drain</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) 45%, transparent);
/* Clock — 6.4s: a 5.12s drain, a 640ms empty hold, a 128ms refill.
The hold exists because zero is a state, not a frame — a bar that
refills the instant it empties never actually ran out. */
--cycle: 6.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);
}
* {
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 {
display: grid;
gap: 12px;
width: min(250px, 78vw);
padding: 18px 20px 20px;
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
background: var(--paper);
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
}
.label {
font-size: 11.5px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--_muted);
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation:
zones var(--t) infinite,
urgency var(--t) var(--ease-in-out) infinite;
}
.rail {
position: relative;
height: 6px;
}
.track {
position: absolute;
inset: 0;
border-radius: 999px;
background: var(--oat);
overflow: hidden;
}
.fill {
position: absolute;
inset: 0;
border-radius: 999px;
transform-origin: left;
animation:
drain var(--t) infinite,
zones var(--t) infinite;
}
/* The thresholds are printed on the track, not the fill: the rules stay
standing after the level has passed them, so how far past the line
things are is always read against the line itself. */
.mark {
position: absolute;
top: -3px;
bottom: -3px;
width: 2px;
border-radius: 999px;
background: color-mix(in srgb, var(--slate) 26%, transparent);
}
.mark-half {
left: 50%;
}
.mark-fifth {
left: 20%;
}
/* Linear, because time remaining is a clock and a clock does not ease.
The refill decelerates — it is the system resetting the window, an
event, not time running backwards. */
@keyframes drain {
0% {
transform: scaleX(1);
animation-timing-function: linear;
}
80%,
90% {
transform: scaleX(0);
animation-timing-function: var(--ease-out);
}
92%,
100% {
transform: scaleX(1);
}
}
/* Colour snaps on the frame the level crosses each mark — 40% and 64%
of the cycle are where a linear drain reaches one half and one fifth.
A blend would claim states between the zones that the rules do not
define. */
@keyframes zones {
0%,
39.9% {
background: var(--olive);
}
40%,
63.9% {
background: var(--clay);
}
64%,
89.9% {
background: var(--rust);
}
90%,
100% {
background: var(--olive);
}
}
/* The dot holds still through two zones and gains a pulse only in the
last — cadence is the escalation the colour alone cannot carry to
someone who is not looking straight at it. 512ms a beat, three beats,
then still for the last 128ms of the zone: it stops when there is
nothing left to escalate, a breath before the reset. */
@keyframes urgency {
0%,
63.9% {
transform: scale(1);
}
68% {
transform: scale(1.35);
}
72% {
transform: scale(1);
}
76% {
transform: scale(1.35);
}
80% {
transform: scale(1);
}
84% {
transform: scale(1.35);
}
88%,
100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
/* The drain is the datum and stays; the dot keeps its colour and
loses its travel. One shared multiplier keeps the snaps landing
exactly where the level crosses the marks. */
.fill,
.dot {
animation-duration: calc(var(--t) * 1.6);
}
.dot {
animation-name: zones;
}
}
</style>
</head>
<body>
<div
class="card"
role="img"
aria-label="A rate-limit window draining linearly along a track with threshold marks at one half and one fifth; the fill snaps olive to clay to rust as it crosses them, a status dot pulses only in the final zone, and the window resets after a beat at empty"
>
<div class="head">
<span class="label">Window resets</span>
<span class="dot"></span>
</div>
<div class="rail">
<span class="track"><span class="fill"></span></span>
<span class="mark mark-half"></span>
<span class="mark mark-fifth"></span>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.