All loaders

CSS / SVG · 3.3 KB gzip

Shimmer Lines

Three skeleton text lines with a diagonal highlight and state-specific terminal styling.

skeletonshimmertextstreamingdependency-free
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/shimmer-lines';

<orbux-shimmer-lines state="thinking"></orbux-shimmer-lines>

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

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

<orbux-shimmer-lines state="thinking"></orbux-shimmer-lines>

Component source shimmer-lines.ts

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

/**
 * Shimmer Lines: three skeleton text lines with a diagonal highlight sweeping
 * across them. The classic "generating a response" content-loading look.
 */
const STYLES = /* css */ `
:host {
  --_dur: 1.8s;
  --_track: color-mix(in srgb, var(--orbux-color, #6366f1) 20%, transparent);
  --_hi: color-mix(in srgb, var(--orbux-color-2, #a855f7) 60%, transparent);
}
.lines {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 12%;
  inline-size: 100%;
  block-size: 100%;
  padding: 8% 6%;
}
.line {
  position: relative;
  overflow: hidden;
  block-size: 14%;
  border-radius: 99px;
  background: var(--_track);
}
.line::after {
  content: '';
  position: absolute;
  inset: 0;
  inline-size: 55%;
  background: linear-gradient(100deg, transparent, var(--_hi), transparent);
  transform: translateX(-120%);
  will-change: transform;
  animation: orbux-shimmer calc(var(--_dur) * var(--orbux-speed-scale)) linear infinite;
}
.line:nth-child(1) { inline-size: 92%; }
.line:nth-child(2) { inline-size: 100%; }
.line:nth-child(3) { inline-size: 64%; }

@keyframes orbux-shimmer {
  to { transform: translateX(300%); }
}

/* ---- states ---- */
:host([data-state="thinking"])     { --_dur: 1.8s; }
:host([data-state="streaming"])    { --_dur: 0.9s; }
:host([data-state="streaming"]) .line:nth-child(3) { inline-size: 82%; }
:host([data-state="tool-calling"]) { --_dur: 1.2s; }
:host([data-state="tool-calling"]) .line { border-radius: 2px; }
:host([data-state="idle"])         { --_dur: 3s; }
:host([data-state="waiting"])      { --_dur: 2.6s; }
:host([data-state="idle"]) .line::after { animation:none; opacity:0; }
:host([data-state="idle"]) .line { opacity: .55; }
:host([data-state="waiting"]) .line::after { animation:none; transform: translateX(40%); opacity: .55; }
:host([data-state="waiting"]) .line:nth-child(2) { opacity: .45; }

:host([data-state="done"]) .line {
  background: color-mix(in srgb, var(--orbux-color-success, #22c55e) 55%, transparent);
}
:host([data-state="done"]) .line::after,
:host([data-state="error"]) .line::after { animation: none; }
:host([data-state="done"]) .lines { animation:orbux-lines-done 220ms ease-out 1; }
:host([data-state="error"]) .lines { animation:orbux-lines-error 200ms ease-out 1; }
@keyframes orbux-lines-done { 50% { transform:scale(1.025); } }
@keyframes orbux-lines-error { 35% { transform:translateX(-2%); } 70% { transform:translateX(2%); } }
:host([data-settled="true"]) .lines { animation:none; }
:host([data-state="error"]) .line {
  background: color-mix(in srgb, var(--orbux-color-error, #ef4444) 50%, transparent);
}

/* ---- run state ---- */
:host([data-paused="true"]) .line::after { animation-play-state: paused; }
:host([data-reduced-motion="true"]) .line {
  background: var(--_track);
}
:host([data-reduced-motion="true"]) .line::after { animation: none !important; }
:host([data-reduced-motion="true"]) .lines { animation:none !important; }
:host([data-reduced-motion="true"][data-state="streaming"]) .line:nth-child(3) { inline-size: 82%; }
:host([data-reduced-motion="true"][data-state="tool-calling"]) .line { border-radius: 2px; }
:host([data-reduced-motion="true"][data-state="waiting"]) .line:nth-child(2) { opacity: .45; }
`;

export class OrbuxShimmerLines extends OrbuxElement {
  protected build(): void {
    const style = document.createElement('style');
    style.textContent = STYLES;
    const lines = document.createElement('div');
    lines.className = 'lines';
    lines.setAttribute('part', 'lines');
    for (let i = 0; i < 3; i++) {
      const line = document.createElement('span');
      line.className = 'line';
      line.setAttribute('part', 'line');
      lines.appendChild(line);
    }
    this.root.append(style, lines);
  }
}

if (typeof customElements !== 'undefined' && !customElements.get('orbux-shimmer-lines')) {
  customElements.define('orbux-shimmer-lines', OrbuxShimmerLines);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-shimmer-lines': OrbuxShimmerLines;
  }
}

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.