Back to the library
Beacon count pulse, cascade
Variant: Tight cascade
The four marks fire 60ms apart near the top of each revolution, reading as one gesture with direction rather than four separate events. Use it when the beacon sits next to other content and you want the count legible at a glance without the row occupying the whole loop. The stagger is deliberately shorter than the flare's own decay, so the lit dots briefly overlap and the group still resolves as a single pulse.
loaderprogressstaggercascadeambient
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>Beacon count pulse — tight cascade</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.2s;
--t: calc(var(--cycle) / var(--speed));
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
}
* { 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);
}
.card {
width: min(420px, 92vw);
padding: clamp(16px, 5vw, 28px);
background: var(--paper);
border: 1px solid var(--oat);
border-radius: calc(14px * var(--round));
display: grid;
justify-items: center;
gap: clamp(14px, 4vw, 20px);
}
.beacon {
position: relative;
width: clamp(104px, 30vw, 128px);
aspect-ratio: 1;
}
.beacon svg { display: block; width: 100%; height: 100%; overflow: visible; }
.track { fill: none; stroke: var(--oat); stroke-width: 3; }
.sweep {
fill: none;
stroke: var(--clay);
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 62 214.5;
transform-origin: 50% 50%;
animation: orbit var(--t) linear infinite;
}
.core {
fill: var(--clay-d);
transform-origin: 50% 50%;
animation: core-breathe var(--t) var(--ease-out) infinite;
}
.halo {
fill: none;
stroke: var(--rust);
stroke-width: 1.5;
transform-origin: 50% 50%;
animation: halo-breathe var(--t) var(--ease-out) infinite;
}
@keyframes orbit {
from { transform: rotate(-90deg); }
to { transform: rotate(270deg); }
}
@keyframes core-breathe {
0%, 100% { transform: scale(0.94); opacity: 0.72; }
14% { transform: scale(1); opacity: 1; }
60% { transform: scale(0.94); opacity: 0.72; }
}
@keyframes halo-breathe {
0%, 100% { transform: scale(0.97); opacity: 0.28; }
16% { transform: scale(1.06); opacity: 0.6; }
62% { transform: scale(0.97); opacity: 0.28; }
}
.tally {
display: flex;
gap: clamp(10px, 3.4vw, 16px);
align-items: center;
}
.tally i {
display: block;
width: clamp(9px, 2.8vw, 11px);
aspect-ratio: 1;
border-radius: 50%;
background: var(--oat);
animation: flare var(--t) var(--ease-out) infinite;
animation-delay: var(--_d);
}
/* 60ms apart — inside the flare's own decay, so the four read as one gesture. */
.tally i:nth-child(1) { --_d: calc(0s / var(--speed)); }
.tally i:nth-child(2) { --_d: calc(0.06s / var(--speed)); }
.tally i:nth-child(3) { --_d: calc(0.12s / var(--speed)); }
.tally i:nth-child(4) { --_d: calc(0.18s / var(--speed)); }
@keyframes flare {
0% { background: var(--oat); transform: scale(0.94); }
9% { background: var(--clay); transform: scale(1); }
34% { background: var(--oat); transform: scale(0.94); }
100% { background: var(--oat); transform: scale(0.94); }
}
.caption {
font-size: clamp(11px, 3vw, 12.5px);
letter-spacing: 0.08em;
color: color-mix(in oklab, var(--slate) 55%, var(--ivory));
}
@media (max-width: 380px) {
.card { padding: 14px; gap: 12px; }
}
@media (prefers-reduced-motion: reduce) {
.sweep { animation: sweep-fade calc(var(--t) * 2.4) var(--ease-out) infinite; }
.core { animation: core-dim calc(var(--t) * 2.4) var(--ease-out) infinite; }
.halo { animation: core-dim calc(var(--t) * 2.4) var(--ease-out) infinite; }
.tally i { animation: tint calc(var(--t) * 2.4) var(--ease-out) infinite; animation-delay: var(--_d); }
@keyframes sweep-fade { 0%, 100% { opacity: 0.35; } 45% { opacity: 0.85; } }
@keyframes core-dim { 0%, 100% { opacity: 0.5; } 45% { opacity: 1; } }
@keyframes tint { 0%, 100% { background: var(--oat); } 45% { background: var(--clay); } }
}
</style>
</head>
<body>
<div class="card">
<div class="beacon">
<svg viewBox="0 0 100 100" aria-hidden="true">
<circle class="track" cx="50" cy="50" r="44" />
<circle class="sweep" cx="50" cy="50" r="44" />
<circle class="halo" cx="50" cy="50" r="20" />
<circle class="core" cx="50" cy="50" r="8" />
</svg>
</div>
<div class="tally" aria-hidden="true">
<i></i><i></i><i></i><i></i>
</div>
<div class="caption">Working</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.