Badge — connection strength
Variant: Connection strength
A four-bar meter climbing to full, dropping to a single bar, and recovering — a live reading of link quality rather than a fixed icon. Only opacity animates: the bar heights are fixed and a lit rung is full clay against the same clay held faint, so counting brightness is counting strength and nothing touches the layout pass. Four identical timelines staggered 150ms apart are what turn a set of blinking bars into a climb up and a recede back down, and the first bar’s delay is zero, so it stays lit through the trough and the meter never reads as fully disconnected. The trough is also the first frame, which is what lets a loop this repetitive close with no seam to catch.
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>Badge — connection strength</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)
);
/* Clock — 3.4s: strength climbs bar by bar, holds full, drops back to a
single bar, holds weak, and the trough is the first frame so the loop
has no seam. --_step is the delay between rungs; it rides --speed, so a
pace roll keeps the climb evenly rungged instead of bunching. */
--cycle: 3.4s;
--t: calc(var(--cycle) / var(--speed));
--_step: calc(150ms / 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);
}
.signal {
display: flex;
align-items: flex-end;
gap: 4px;
height: 26px;
}
/* Heights are fixed and never animated — only opacity moves, so the meter
stays off the layout pass. A lit bar is full clay, an unlit one the same
clay held faint, which is why counting brightness reads as strength. */
.bar {
width: 6px;
border-radius: calc(2px * var(--round));
background: var(--clay);
opacity: 0.16;
animation: rung var(--t) var(--ease-out) infinite;
}
.bar:nth-child(1) {
height: 8px;
}
.bar:nth-child(2) {
height: 14px;
animation-delay: var(--_step);
}
.bar:nth-child(3) {
height: 20px;
animation-delay: calc(var(--_step) * 2);
}
.bar:nth-child(4) {
height: 26px;
animation-delay: calc(var(--_step) * 3);
}
/* Each rung lights, holds through the plateau, then falls — the stagger
delays turn four identical timelines into a climb up and a recede down.
The first bar's delay is zero, so it is lit at the trough and the meter
never reads as fully disconnected. */
@keyframes rung {
0%,
6% {
opacity: 0.16;
}
20%,
56% {
opacity: 1;
}
72%,
100% {
opacity: 0.16;
}
}
@media (prefers-reduced-motion: reduce) {
/* Opacity is the whole animation, so nothing has to be removed — only
calmed: the cycle stretches and the trough lifts so the rungs fade
between strong and weak rather than snapping dark. */
.bar {
animation-name: rung-calm;
animation-duration: calc(var(--t) * 1.7);
}
.bar:nth-child(2) {
animation-delay: calc(var(--_step) * 1.7);
}
.bar:nth-child(3) {
animation-delay: calc(var(--_step) * 3.4);
}
.bar:nth-child(4) {
animation-delay: calc(var(--_step) * 5.1);
}
@keyframes rung-calm {
0%,
6% {
opacity: 0.34;
}
20%,
56% {
opacity: 1;
}
72%,
100% {
opacity: 0.34;
}
}
}
</style>
</head>
<body>
<div
class="signal"
role="img"
aria-label="A four-bar connection meter climbing to full strength, dropping to one bar, and recovering"
>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.