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
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
| 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.