CSS / SVG · 3.4 KB gzip
Radar Ping
Concentric rings expanding from a bright core like a sonar ping: faster while streaming, calm on done.
radarpingripplepulsedependency-free
Interactive preview
Usage
Packages are @vikast908/core and
@vikast908/loaders. Prefer 0.1.1+
(0.1.0 loaders is broken on npm).
npm install @vikast908/loaders@0.1.1 @vikast908/core@0.1.1
import '@vikast908/loaders/radar-ping'; <orbux-radar-ping state="thinking"></orbux-radar-ping>
Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:
<script type="module" src="http://localhost:4321/orbux/radar-ping.js"></script> <orbux-radar-ping state="thinking"></orbux-radar-ping>
Component source radar-ping.ts
import { OrbuxElement } from '@vikast908/core';
/**
* Radar Ping: concentric rings expanding outward from a bright core, like a
* sonar ping. Pings faster while streaming, calms on done, goes irregular red
* on error.
*/
const STYLES = /* css */ `
:host { --_dur: 2s; }
.stage {
position: relative;
inline-size: 100%;
block-size: 100%;
}
.ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid var(--orbux-color, #6366f1);
transform: scale(0.1);
opacity: 0;
animation: orbux-ping calc(var(--_dur) * var(--orbux-speed-scale)) ease-out infinite;
}
.ring:nth-child(2) { animation-delay: calc(var(--_dur) * 0.33 * var(--orbux-speed-scale)); }
.ring:nth-child(3) { animation-delay: calc(var(--_dur) * 0.66 * var(--orbux-speed-scale)); }
.dot {
position: absolute;
inset-inline-start: 50%;
inset-block-start: 50%;
inline-size: 18%;
aspect-ratio: 1;
border-radius: 50%;
transform: translate(-50%, -50%);
background: radial-gradient(circle at 30% 30%, var(--orbux-color-2, #a855f7), var(--orbux-color, #6366f1));
box-shadow: 0 0 8px color-mix(in srgb, var(--orbux-color, #6366f1) 70%, transparent);
}
@keyframes orbux-ping {
0% { transform: scale(0.1); opacity: 0.85; }
80% { opacity: 0; }
100% { transform: scale(1); opacity: 0; }
}
/* ---- states ---- */
:host([data-state="thinking"]) { --_dur: 2s; }
:host([data-state="streaming"]) { --_dur: 1.1s; }
:host([data-state="streaming"]) .ring { border-style: solid; border-width: 2.5px; }
:host([data-state="tool-calling"]) { --_dur: 1.4s; }
:host([data-state="tool-calling"]) .ring { border-radius: 28%; border-style: dashed; animation-timing-function: steps(4, end); }
:host([data-state="idle"]) { --_dur: 3.4s; }
:host([data-state="waiting"]) { --_dur: 3s; }
:host([data-state="idle"]) .ring { animation:none;transform:scale(.55);opacity:.18; }
:host([data-state="idle"]) .stage { opacity: 0.5; }
:host([data-state="waiting"]) .ring { animation:none;transform:scale(.82);opacity:.4; }
:host([data-state="waiting"]) .ring:nth-child(2) { opacity: .22; }
:host([data-state="waiting"]) .stage { opacity: 0.82; }
:host([data-state="done"]) .ring { animation: none; opacity: 0; }
:host([data-state="done"]) .dot {
background: var(--orbux-color-success, #22c55e);
box-shadow: 0 0 14px var(--orbux-color-success, #22c55e);
}
:host([data-state="error"]) .ring {
border-color: var(--orbux-color-error, #ef4444);
animation: orbux-ping-error 200ms ease-out 1;
}
:host([data-state="error"]) .dot {
background: var(--orbux-color-error, #ef4444);
box-shadow: 0 0 12px var(--orbux-color-error, #ef4444);
}
@keyframes orbux-ping-error { 40% { transform: scale(.58); opacity:.8; } 100% { transform:scale(.7);opacity:.3; } }
:host([data-settled="true"]) .ring { animation: none; }
/* ---- run state ---- */
:host([data-paused="true"]) .ring { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .ring {
animation: none !important;
transform: scale(0.7);
opacity: 0.3;
}
:host([data-reduced-motion="true"][data-state="streaming"]) .ring:nth-child(2) { transform:scale(.48); }
:host([data-reduced-motion="true"][data-state="tool-calling"]) .ring { border-radius:24%; }
:host([data-reduced-motion="true"][data-state="waiting"]) .ring:nth-child(3) { opacity:.12; }
`;
export class OrbuxRadarPing extends OrbuxElement {
protected build(): void {
const style = document.createElement('style');
style.textContent = STYLES;
const stage = document.createElement('div');
stage.className = 'stage';
stage.setAttribute('part', 'stage');
stage.innerHTML =
'<span class="ring" part="ring"></span>' +
'<span class="ring" part="ring"></span>' +
'<span class="ring" part="ring"></span>' +
'<span class="dot" part="core"></span>';
this.root.append(style, stage);
}
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-radar-ping')) {
customElements.define('orbux-radar-ping', OrbuxRadarPing);
}
declare global {
interface HTMLElementTagNameMap {
'orbux-radar-ping': OrbuxRadarPing;
}
}
Depends on @vikast908/core. Paste the source, add the base
element, and set state from your agent code.
Attributes & properties
| Name | Type | Description |
|---|---|---|
state | AgentState | idle · thinking · streaming · tool-calling · waiting · done · error |
size | CSS length | Overall square size. |
--orbux-width | CSS length | Optional inline-size override. |
--orbux-height | CSS length | Optional block-size override. |
speed | number | Animation speed multiplier (1 = default). |
label | string | Accessible label (defaults per state). |
paused | boolean | Freeze the animation. |
Respects prefers-reduced-motion, exposes
status/progressbar semantics with a live label, and auto-pauses (sets
data-paused) when offscreen or the tab is hidden.