All loaders

CSS / SVG · 3.4 KB gzip

Morph Blob

A gradient blob that fluidly morphs and rotates: energetic while streaming, blockier for tools, a smooth green circle on done.

blobmorphorganicgradient
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/morph-blob';

<orbux-morph-blob state="thinking"></orbux-morph-blob>

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

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

<orbux-morph-blob state="thinking"></orbux-morph-blob>

Component source morph-blob.ts

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

/**
 * Morph Blob: a gradient blob that fluidly morphs its shape while slowly
 * rotating. Faster and more energetic while streaming, blockier while calling a
 * tool, a smooth green circle on `done`, red and jittery on `error`.
 */
const STYLES = /* css */ `
:host { --_dur: 7s; }
.wrap { display: grid; place-items: center; inline-size: 100%; block-size: 100%; }
.blob {
  inline-size: 74%; aspect-ratio: 1;
  background: radial-gradient(circle at 32% 28%, var(--orbux-color-2, #a855f7), var(--orbux-color, #6366f1));
  box-shadow: 0 0 18px color-mix(in srgb, var(--orbux-color, #6366f1) 40%, transparent);
  animation:
    orbux-morph calc(var(--_dur) * var(--orbux-speed-scale)) ease-in-out infinite,
    orbux-rot calc(var(--_dur) * 1.6 * var(--orbux-speed-scale)) linear infinite;
}
@keyframes orbux-morph {
  0%, 100% { border-radius: 42% 58% 63% 37% / 41% 44% 56% 59%; }
  25%      { border-radius: 62% 38% 41% 59% / 58% 62% 38% 42%; }
  50%      { border-radius: 38% 62% 55% 45% / 49% 38% 62% 51%; }
  75%      { border-radius: 55% 45% 38% 62% / 62% 51% 49% 38%; }
}
@keyframes orbux-rot { to { transform: rotate(360deg); } }

/* ---- states ---- */
:host([data-state="thinking"])  { --_dur: 6s; }
:host([data-state="streaming"]) { --_dur: 3s; }
:host([data-state="streaming"]) .blob { filter: saturate(1.2) brightness(1.06); }
:host([data-state="idle"])      { --_dur: 12s; }
:host([data-state="waiting"])   { --_dur: 10s; }
:host([data-state="idle"]) .blob {
  animation: none;
  opacity: .52;
  border-radius: 42% 58% 63% 37% / 41% 44% 56% 59%;
}
:host([data-state="waiting"]) .blob {
  animation: none;
  opacity: .82;
  border-radius: 55% 45% 38% 62% / 62% 51% 49% 38%;
  transform: scale(.92);
}
:host([data-state="tool-calling"]) .blob {
  animation: orbux-rot calc(var(--_dur) * 0.8 * var(--orbux-speed-scale)) steps(6, end) infinite;
  border-radius: 24%;
}

:host([data-state="done"]) .blob {
  animation: none;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 28%, color-mix(in srgb, var(--orbux-color-success, #22c55e) 60%, white), var(--orbux-color-success, #22c55e));
  box-shadow: 0 0 20px color-mix(in srgb, var(--orbux-color-success, #22c55e) 55%, transparent);
  transition: border-radius 220ms ease-out;
}
:host([data-state="error"]) { --_dur: 2s; }
:host([data-state="error"]) .blob {
  background: radial-gradient(circle at 32% 28%, color-mix(in srgb, var(--orbux-color-error, #ef4444) 55%, white), var(--orbux-color-error, #ef4444));
  box-shadow: 0 0 18px color-mix(in srgb, var(--orbux-color-error, #ef4444) 50%, transparent);
  animation: orbux-morph-spiky 200ms ease-out 1;
}
:host([data-settled="true"]) .blob { animation:none; }
@keyframes orbux-morph-spiky {
  0%, 100% { border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%; }
  50%      { border-radius: 30% 70% 70% 30% / 40% 60% 40% 60%; }
}

:host([data-paused="true"]) .blob { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .blob { animation: none !important; border-radius: 46% 54% 58% 42% / 44% 48% 52% 56%; }
:host([data-reduced-motion="true"][data-state="streaming"]) .blob { border-radius:38% 62% 38% 62%; }
:host([data-reduced-motion="true"][data-state="tool-calling"]) .blob { border-radius:24%; }
:host([data-reduced-motion="true"][data-state="waiting"]) .blob { opacity:.65; }
`;

export class OrbuxMorphBlob extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const wrap = document.createElement('div');
    wrap.className = 'wrap';
    const blob = document.createElement('div');
    blob.className = 'blob';
    blob.setAttribute('part', 'blob');
    wrap.appendChild(blob);
    this.root.append(style, wrap);
  }
}

if (typeof customElements !== 'undefined' && !customElements.get('orbux-morph-blob')) {
  customElements.define('orbux-morph-blob', OrbuxMorphBlob);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-morph-blob': OrbuxMorphBlob;
  }
}

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.