CSS / SVG · 3.5 KB gzip
Wave Bars
Five equalizer bars bouncing in a wave: tall and fast while streaming, flat green on done.
equalizeraudiobarsinlinedependency-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/wave-bars'; <orbux-wave-bars state="thinking"></orbux-wave-bars>
Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:
<script type="module" src="http://localhost:4321/orbux/wave-bars.js"></script> <orbux-wave-bars state="thinking"></orbux-wave-bars>
Component source wave-bars.ts
import { type AgentState, OrbuxElement } from '@vikast908/core';
const TERMINAL_SCALES: Partial<Record<AgentState, readonly number[]>> = {
done: [0.42, 0.42, 0.42, 0.42, 0.42],
error: [0.5, 0.95, 0.35, 0.8, 0.45],
};
/**
* Wave Bars: five equalizer bars bouncing in a wave. Tall and fast while
* streaming, calm while idle, flat green on done, jagged red on error.
*/
const STYLES = /* css */ `
:host { --_dur: 1s; --_min: 0.3; }
.bars {
display: flex;
gap: 12%;
align-items: center;
justify-content: center;
inline-size: 100%;
block-size: 100%;
}
.bar {
inline-size: 11%;
block-size: 72%;
border-radius: 99px;
transform: scaleY(var(--_min));
background: linear-gradient(var(--orbux-color-2, #a855f7), var(--orbux-color, #6366f1));
animation: orbux-wave calc(var(--_dur) * var(--orbux-speed-scale)) ease-in-out infinite;
transition: transform 180ms ease-out, opacity 180ms ease-out;
}
.bar:nth-child(1) { animation-delay: calc(var(--_dur) * 0.0 * var(--orbux-speed-scale)); }
.bar:nth-child(2) { animation-delay: calc(var(--_dur) * 0.12 * var(--orbux-speed-scale)); }
.bar:nth-child(3) { animation-delay: calc(var(--_dur) * 0.24 * var(--orbux-speed-scale)); }
.bar:nth-child(4) { animation-delay: calc(var(--_dur) * 0.12 * var(--orbux-speed-scale)); }
.bar:nth-child(5) { animation-delay: calc(var(--_dur) * 0.0 * var(--orbux-speed-scale)); }
@keyframes orbux-wave {
0%, 100% { transform: scaleY(var(--_min)); }
50% { transform: scaleY(1); }
}
/* ---- states ---- */
:host([data-state="thinking"]) { --_dur: 1s; --_min: 0.32; }
:host([data-state="streaming"]) { --_dur: 0.55s; --_min: 0.16; }
:host([data-state="tool-calling"]) { --_dur: 0.7s; --_min: 0.24; }
:host([data-state="idle"]) { --_dur: 2.2s; --_min: 0.5; }
:host([data-state="waiting"]) { --_dur: 1.9s; --_min: 0.55; }
:host([data-state="idle"]) .bar,
:host([data-state="waiting"]) .bar { animation:none;transform:scaleY(var(--_min)); }
:host([data-state="done"]) .bar {
background: var(--orbux-color-success, #22c55e);
animation: none;
}
:host([data-state="error"]) .bar {
background: var(--orbux-color-error, #ef4444);
animation: none;
}
:host([data-settled="true"]) .bar { animation: none; }
/* ---- run state ---- */
:host([data-paused="true"]) .bar { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .bar {
animation: none !important;
transform: scaleY(0.6);
transition: none;
}
:host([data-reduced-motion="true"][data-state="thinking"]) .bar:nth-child(3) { transform:scaleY(.82); }
:host([data-reduced-motion="true"][data-state="streaming"]) .bar:nth-child(2),
:host([data-reduced-motion="true"][data-state="streaming"]) .bar:nth-child(4) { transform:scaleY(.9); }
:host([data-reduced-motion="true"][data-state="tool-calling"]) .bar { border-radius:2px; }
:host([data-reduced-motion="true"][data-state="waiting"]) .bar { opacity:.48; }
:host([data-reduced-motion="true"][data-state="done"]) .bar { transform: scaleY(0.42); }
:host([data-reduced-motion="true"][data-state="error"]) .bar:nth-child(1) { transform: scaleY(0.5); }
:host([data-reduced-motion="true"][data-state="error"]) .bar:nth-child(2) { transform: scaleY(0.95); }
:host([data-reduced-motion="true"][data-state="error"]) .bar:nth-child(3) { transform: scaleY(0.35); }
:host([data-reduced-motion="true"][data-state="error"]) .bar:nth-child(4) { transform: scaleY(0.8); }
:host([data-reduced-motion="true"][data-state="error"]) .bar:nth-child(5) { transform: scaleY(0.45); }
`;
export class OrbuxWaveBars extends OrbuxElement {
#bars: HTMLElement[] = [];
#settleFrame = 0;
#opacityBridges: Animation[] = [];
protected build(): void {
const style = document.createElement('style');
style.textContent = STYLES;
const bars = document.createElement('div');
bars.className = 'bars';
bars.setAttribute('part', 'bars');
for (let i = 0; i < 5; i++) {
const bar = document.createElement('span');
bar.className = 'bar';
bar.setAttribute('part', 'bar');
this.#bars.push(bar);
bars.appendChild(bar);
}
this.root.append(style, bars);
}
protected override onStateChange(next: AgentState): void {
if (this.#settleFrame) cancelAnimationFrame(this.#settleFrame);
this.#settleFrame = 0;
for (const animation of this.#opacityBridges) animation.cancel();
this.#opacityBridges = [];
const scales = TERMINAL_SCALES[next];
if (!scales) {
for (const bar of this.#bars) {
bar.style.removeProperty('animation');
bar.style.removeProperty('transform');
}
return;
}
for (const bar of this.#bars) {
const current = getComputedStyle(bar).transform;
bar.style.transform = current === 'none' ? 'scaleY(1)' : current;
bar.style.animation = 'none';
}
if (this.reducedMotion) {
this.#applyScales(scales);
this.#opacityBridges = this.#bars.map((bar) =>
bar.animate([{ opacity: 0.72 }, { opacity: 1 }], {
duration: 100,
easing: 'ease-out',
}),
);
return;
}
this.#settleFrame = requestAnimationFrame(() => {
this.#settleFrame = 0;
if (this.state === next) this.#applyScales(scales);
});
}
#applyScales(scales: readonly number[]): void {
for (const [index, bar] of this.#bars.entries()) {
bar.style.transform = `scaleY(${scales[index] ?? 0.42})`;
}
}
override disconnectedCallback(): void {
if (this.#settleFrame) cancelAnimationFrame(this.#settleFrame);
this.#settleFrame = 0;
for (const animation of this.#opacityBridges) animation.cancel();
this.#opacityBridges = [];
super.disconnectedCallback();
}
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-wave-bars')) {
customElements.define('orbux-wave-bars', OrbuxWaveBars);
}
declare global {
interface HTMLElementTagNameMap {
'orbux-wave-bars': OrbuxWaveBars;
}
}
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.