Back to the library
Guest arrival ring
Variant: Standard
An ambient presence indicator: an outer ring opens outward to receive an incoming avatar chip, which settles into the centre while three companion dots fill in around it. Reach for it on a waiting-room, shared-session, or joining-call surface where you want to show that a new participant is being received rather than merely counted. The ring expansion leads the chip by 90ms so the space is visibly made ready before the arrival lands, which is the whole point of the gesture.
loaderpresenceambientringstatus
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>Guest arrival ring — standard</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.6s;
--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);
/* File-specific */
--_ring-w: 2px;
--_ring-open: 0.86;
--_ring-wide: 1.06;
--_chip-travel: 26%;
--_dot-size: 9px;
--_dot-dim: 0.22;
--_stage: min(300px, 82vw);
}
* { 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(360px, 92vw);
padding: clamp(18px, 5vw, 26px);
border-radius: calc(18px * var(--round));
background: var(--paper);
border: 1px solid var(--oat);
display: grid;
justify-items: center;
gap: clamp(12px, 3.5vw, 18px);
}
.stage {
position: relative;
width: var(--_stage);
height: calc(var(--_stage) * 0.52);
display: grid;
place-items: center;
}
/* The receiving ring: opens outward ahead of the guest, then holds. */
.ring {
position: absolute;
width: calc(var(--_stage) * 0.42);
height: calc(var(--_stage) * 0.42);
border-radius: 50%;
border: var(--_ring-w) solid var(--clay);
opacity: 0;
transform: scale(var(--_ring-open));
animation: ring-open var(--t) var(--ease-out) infinite;
}
.ring.echo {
border-color: var(--olive);
animation-delay: calc(140ms / var(--speed));
}
@keyframes ring-open {
0% { transform: scale(var(--_ring-open)); opacity: 0; }
14% { transform: scale(var(--_ring-open)); opacity: 0.9; }
46% { transform: scale(var(--_ring-wide)); opacity: 0.55; }
74% { transform: scale(var(--_ring-wide)); opacity: 0.45; }
92% { transform: scale(var(--_ring-open)); opacity: 0; }
100% { transform: scale(var(--_ring-open)); opacity: 0; }
}
/* The arriving guest, entering from the edge of the frame. */
.chip {
position: absolute;
width: calc(var(--_stage) * 0.2);
height: calc(var(--_stage) * 0.2);
border-radius: 50%;
background: var(--clay-d);
display: grid;
place-items: center;
opacity: 0;
/* enters 90ms after the ring begins to open — space made ready first */
animation: chip-arrive var(--t) var(--ease-out) calc(90ms / var(--speed)) infinite;
}
.chip svg { width: 52%; height: 52%; display: block; }
.chip path { fill: none; stroke: var(--paper); stroke-width: 2.1; stroke-linecap: round; stroke-linejoin: round; }
@keyframes chip-arrive {
0% { transform: translateX(calc(-1 * var(--_chip-travel))) scale(0.92); opacity: 0; }
22% { transform: translateX(0) scale(1); opacity: 1; }
76% { transform: translateX(0) scale(1); opacity: 1; }
90% { transform: translateX(0) scale(0.94); opacity: 0; }
100% { transform: translateX(0) scale(0.94); opacity: 0; }
}
/* Companions already seated: they brighten in turn once the guest lands. */
.seats {
position: absolute;
inset: 0;
display: grid;
place-items: center;
pointer-events: none;
}
.seat {
position: absolute;
width: var(--_dot-size);
height: var(--_dot-size);
border-radius: 50%;
background: var(--rust);
opacity: var(--_dot-dim);
transform: rotate(var(--_a)) translateX(calc(var(--_stage) * 0.3)) rotate(calc(-1 * var(--_a)));
animation: seat-fill var(--t) var(--ease-in-out) var(--_d) infinite;
}
.seat:nth-child(1) { --_a: -46deg; --_d: calc(360ms / var(--speed)); }
.seat:nth-child(2) { --_a: 0deg; --_d: calc(420ms / var(--speed)); }
.seat:nth-child(3) { --_a: 46deg; --_d: calc(480ms / var(--speed)); }
@keyframes seat-fill {
0% { opacity: var(--_dot-dim); }
16% { opacity: 1; }
70% { opacity: 1; }
88% { opacity: var(--_dot-dim); }
100% { opacity: var(--_dot-dim); }
}
.label {
font-size: clamp(11px, 3vw, 13px);
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--slate);
opacity: 0.62;
}
@media (max-width: 380px) {
.card { padding: 14px; gap: 10px; }
}
@media (prefers-reduced-motion: reduce) {
/* Movement drops; the receiving/seating rhythm survives as opacity only. */
.ring {
transform: scale(1);
animation: ring-hold calc(var(--t) * 2.2) var(--ease-in-out) infinite;
}
.ring.echo { animation-delay: calc(300ms / var(--speed)); }
.chip {
transform: none;
animation: soft-hold calc(var(--t) * 2.2) var(--ease-in-out) calc(200ms / var(--speed)) infinite;
}
.seat {
transform: rotate(var(--_a)) translateX(calc(var(--_stage) * 0.3)) rotate(calc(-1 * var(--_a)));
animation: seat-fill calc(var(--t) * 2.2) var(--ease-in-out) var(--_d) infinite;
}
@keyframes ring-hold {
0%, 100% { opacity: 0.35; }
50% { opacity: 0.85; }
}
@keyframes soft-hold {
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
}
</style>
</head>
<body>
<div class="card">
<div class="stage">
<div class="ring"></div>
<div class="ring echo"></div>
<div class="seats">
<span class="seat"></span>
<span class="seat"></span>
<span class="seat"></span>
</div>
<div class="chip">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 4.5a3.4 3.4 0 1 1 0 6.8 3.4 3.4 0 0 1 0-6.8Z" />
<path d="M4.8 19.4c0-3.3 3.2-5.3 7.2-5.3s7.2 2 7.2 5.3" />
</svg>
</div>
</div>
<div class="label">Someone is joining</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.