All loaders

CSS / SVG · 3.3 KB gzip

Dot Grid

A 3×3 grid of dots doing a diagonal wave ripple: faster while streaming, a checkerboard pulse for tools, all-green on done.

griddotswaveminimal
Interactive preview

Usage

Download HTML

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/dot-grid';

<orbux-dot-grid state="thinking"></orbux-dot-grid>

Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:

<script type="module" src="http://localhost:4321/orbux/dot-grid.js"></script>

<orbux-dot-grid state="thinking"></orbux-dot-grid>

Component source dot-grid.ts

import { OrbuxElement } from '@vikast908/core';

/**
 * Dot Grid: a 3×3 grid of dots doing a diagonal wave ripple. Faster while
 * streaming, a checkerboard pulse while calling a tool, all-green on `done`,
 * staggered red snap on `error`.
 */
const STYLES = /* css */ `
:host { --_dur: 1.4s; }
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  gap: 14%;
  inline-size: 100%; block-size: 100%;
  place-items: center;
  padding: 10%;
  box-sizing: border-box;
}
.grid i {
  inline-size: 100%; aspect-ratio: 1; border-radius: 50%;
  background: var(--orbux-color, #6366f1);
  animation: orbux-dg calc(var(--_dur) * var(--orbux-speed-scale)) ease-in-out infinite;
  animation-delay: calc(var(--_dur) * var(--d, 0) * 0.13 * var(--orbux-speed-scale));
}
@keyframes orbux-dg {
  0%, 60%, 100% { transform: scale(0.45); opacity: 0.35; }
  30%           { transform: scale(1);    opacity: 1;    }
}

/* ---- states ---- */
:host([data-state="thinking"])  { --_dur: 1.4s; }
:host([data-state="streaming"]) { --_dur: 0.85s; }
:host([data-state="streaming"]) .grid i { border-radius: 35%; }
:host([data-state="idle"]),
:host([data-state="waiting"])   { --_dur: 2.6s; }
:host([data-state="idle"]) .grid i { animation: none; opacity: .38; transform:scale(.55); }
:host([data-state="waiting"]) .grid i { animation: none; opacity: .72; transform:scale(.78); }
:host([data-state="waiting"]) .grid i:nth-child(even) { opacity: .28; }
:host([data-state="tool-calling"]) { --_dur: 1s; }
:host([data-state="tool-calling"]) .grid i {
  border-radius: 22%;
  animation-delay: calc(var(--_dur) * var(--p, 0) * 0.5 * var(--orbux-speed-scale));
  animation-timing-function: steps(2, end);
}

:host([data-state="done"]) .grid i {
  animation: none;
  transform: scale(0.9);
  opacity: 1;
  background: var(--orbux-color-success, #22c55e);
}
:host([data-state="error"]) { --_dur: 0.5s; }
:host([data-state="error"]) .grid i {
  background: var(--orbux-color-error, #ef4444);
  animation: orbux-dg-flicker 220ms ease-out 1;
  animation-delay: calc(var(--d, 0) * 28ms);
}
@keyframes orbux-dg-flicker {
  0%, 100% { opacity: 1; transform: scale(0.9); }
  35% { opacity: 0.25; transform: scale(0.45); }
  70% { opacity: 1; transform: scale(1.05); }
}
:host([data-state="error"]) .grid { animation:orbux-dg-error 200ms ease-out 1; }
@keyframes orbux-dg-error { 35% { transform:translateX(-5%); } 70% { transform:translateX(5%); } }
:host([data-settled="true"]) .grid,
:host([data-settled="true"]) .grid i { animation:none; }
:host([data-settled="true"][data-state="error"]) .grid i { opacity: 1; transform: scale(0.9); }

:host([data-paused="true"]) .grid i { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .grid i { animation: none !important; transform: none; opacity: 0.8; }
:host([data-reduced-motion="true"][data-state="thinking"]) .grid i:nth-child(even) { opacity:.45; }
:host([data-reduced-motion="true"][data-state="streaming"]) .grid i { opacity:1; }
:host([data-reduced-motion="true"][data-state="tool-calling"]) .grid i { border-radius:22%; }
:host([data-reduced-motion="true"][data-state="waiting"]) .grid i:nth-child(n+7) { opacity:.25; }
`;

export class OrbuxDotGrid extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const grid = document.createElement('div');
    grid.className = 'grid';
    grid.setAttribute('part', 'grid');
    for (let r = 0; r < 3; r++) {
      for (let c = 0; c < 3; c++) {
        const dot = document.createElement('i');
        dot.style.setProperty('--d', String(r + c));
        dot.style.setProperty('--p', String((r + c) % 2));
        grid.appendChild(dot);
      }
    }
    this.root.append(style, grid);
  }
}

if (typeof customElements !== 'undefined' && !customElements.get('orbux-dot-grid')) {
  customElements.define('orbux-dot-grid', OrbuxDotGrid);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-dot-grid': OrbuxDotGrid;
  }
}

Depends on @vikast908/core. Paste the source, add the base element, and set state from your agent code.

Attributes & properties

NameTypeDescription
stateAgentState idle · thinking · streaming · tool-calling · waiting · done · error
sizeCSS lengthOverall square size.
--orbux-widthCSS lengthOptional inline-size override.
--orbux-heightCSS lengthOptional block-size override.
speednumberAnimation speed multiplier (1 = default).
labelstringAccessible label (defaults per state).
pausedbooleanFreeze 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.