All loaders

CSS / SVG · 3.3 KB gzip

Skeleton Stack

A configurable text skeleton with composited shimmer and static reduced-motion state cues.

skeletoncontenttextconfigurable
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/skeleton-stack';

<orbux-skeleton-stack state="thinking"></orbux-skeleton-stack>

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

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

<orbux-skeleton-stack state="thinking"></orbux-skeleton-stack>

Component source skeleton-stack.ts

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

const STYLES = /* css */ `
:host{inline-size:var(--orbux-width,var(--orbux-size,180px));block-size:var(--orbux-height,72px)}
.stack{display:grid;align-content:center;gap:12%;inline-size:100%;block-size:100%}
.line{position:relative;overflow:hidden;block-size:12%;min-block-size:6px;border-radius:999px;background:color-mix(in srgb,var(--orbux-color,#6366f1) 15%,transparent)}
.line:last-child{inline-size:66%}.line::after{content:"";position:absolute;inset:0;inline-size:48%;background:linear-gradient(100deg,transparent,color-mix(in srgb,var(--orbux-color-2,#a855f7) 48%,transparent),transparent);transform:translateX(-120%);animation:orbux-skeleton calc(1.65s * var(--orbux-speed-scale)) linear infinite}
@keyframes orbux-skeleton{to{transform:translateX(310%)}}
:host([data-state="streaming"]) .line::after{animation-duration:calc(.9s * var(--orbux-speed-scale))}
:host([data-state="tool-calling"]) .line{border-radius:2px}
:host([data-state="waiting"]) .line{opacity:.55}
:host([data-state="idle"]) .line::after,:host([data-state="waiting"]) .line::after{animation:none}
:host([data-state="done"]) .line{background:color-mix(in srgb,var(--orbux-color-success,#22c55e) 48%,transparent)}
:host([data-state="error"]) .line{background:color-mix(in srgb,var(--orbux-color-error,#ef4444) 44%,transparent)}
:host([data-state="done"]) .line::after,:host([data-state="error"]) .line::after{animation:none}
:host([data-state="done"]) .stack{animation:orbux-skeleton-done 220ms ease-out 1}
:host([data-state="error"]) .stack{animation:orbux-skeleton-error 200ms ease-out 1}
@keyframes orbux-skeleton-done{50%{transform:scale(1.025)}}
@keyframes orbux-skeleton-error{35%{transform:translateX(-2%)}70%{transform:translateX(2%)}}
:host([data-settled="true"]) .stack{animation:none}
:host([data-paused="true"]) .line::after{animation-play-state:paused}
:host([data-reduced-motion="true"]) .line::after{animation:none}
:host([data-reduced-motion="true"]) .stack{animation:none}
:host([data-reduced-motion="true"][data-state="streaming"]) .line:nth-child(even){opacity:.62}
:host([data-reduced-motion="true"][data-state="thinking"]) .line:nth-child(2){inline-size:86%}
:host([data-reduced-motion="true"][data-state="idle"]) .line{opacity:.5}
`;

export class OrbuxSkeletonStack extends OrbuxElement {
  static override readonly observedAttributes = [...OrbuxElement.observedAttributes, 'lines'];
  #stack: HTMLElement | null = null;

  get lines(): number {
    return Math.min(6, Math.max(1, Number(this.getAttribute('lines')) || 3));
  }
  set lines(value: number) {
    this.setAttribute('lines', String(value));
  }

  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    this.#stack = document.createElement('span');
    this.#stack.className = 'stack';
    this.#stack.setAttribute('part', 'stack');
    this.root.append(style, this.#stack);
    this.#renderLines();
  }

  override attributeChangedCallback(
    name: string,
    oldValue: string | null,
    value: string | null,
  ): void {
    super.attributeChangedCallback(name, oldValue, value);
    if (name === 'lines' && oldValue !== value) this.#renderLines();
  }

  #renderLines(): void {
    if (!this.#stack) return;
    this.#stack.replaceChildren(
      ...Array.from({ length: this.lines }, () => {
        const line = document.createElement('i');
        line.className = 'line';
        line.setAttribute('part', 'line');
        return line;
      }),
    );
  }
}

if (typeof customElements !== 'undefined' && !customElements.get('orbux-skeleton-stack')) {
  customElements.define('orbux-skeleton-stack', OrbuxSkeletonStack);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-skeleton-stack': OrbuxSkeletonStack;
  }
}

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.