# Loading State

> A pixel-grid loader with a shimmering label and live elapsed timer.

A shadcn/ui block from the [@spectrumui](https://ui.spectrumhq.in) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/spectrumui/loading-state).

- Page, with a live preview: https://shadcn.exchange/r/spectrumui/loading-state
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/spectrumui/loading-state-light-fa423bf294deba82.png
- Registry item: https://ui.spectrumhq.in/r/loading-state.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.spectrumhq.in/r/loading-state.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/spectrumui/blocks/ai-assistants/loading-state.tsx`

```tsx
"use client";

import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";

const KEYFRAMES = `
@keyframes su-pixel-on { 0%, 100% { opacity: 0.15 } 40% { opacity: 1 } }
@keyframes su-shimmer-text { 0% { background-position: 200% 0 } 100% { background-position: -200% 0 } }
`;

const chevron = Array.from({ length: 9 }, (_, i) => {
  const r = Math.floor(i / 3),
    c = i % 3;
  return (c + Math.abs(r - 1)) * 90;
});

const ORBIT_ORDER = [0, 1, 2, 5, 8, 7, 6, 3];
const orbit = Array.from({ length: 9 }, (_, i) => {
  const k = ORBIT_ORDER.indexOf(i);
  return k === -1 ? null : k * 110;
});

export type LoadingStateVariant = "Drive" | "Dots" | "Orbit";

const PATTERNS: Record<
  LoadingStateVariant,
  { delays: (number | null)[]; dur: number; round: boolean }
> = {
  Drive: { delays: chevron, dur: 650, round: false },
  Dots: { delays: chevron, dur: 650, round: true },
  Orbit: { delays: orbit, dur: 950, round: false },
};

function useElapsed() {
  const [ds, setDs] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setDs((d) => d + 1), 100);
    return () => clearInterval(t);
  }, []);
  const total = ds / 10;
  if (total < 60) return `${total.toFixed(1)}s`;
  return `${Math.floor(total / 60)}m ${(total % 60).toFixed(1)}s`;
}

export interface LoadingStateProps {
  label?: string;
  variant?: LoadingStateVariant;
  className?: string;
}

export function LoadingState({
  label = "Churning",
  variant = "Drive",
  className,
}: LoadingStateProps) {
  const elapsed = useElapsed();
  const { delays, dur, round } = PATTERNS[variant] ?? PATTERNS.Drive;

  return (
    <div
      className={cn(
        "flex w-fit items-center gap-2.5 text-neutral-900 dark:text-neutral-100",
        className,
      )}
    >
      <style dangerouslySetInnerHTML={{ __html: KEYFRAMES }} />

      <span aria-hidden className="grid grid-cols-[repeat(3,4px)] gap-[1.5px]">
        {delays.map((d, i) => (
          <span
            key={i}
            className={cn(
              "size-[4px] bg-current motion-reduce:!animate-none",
              round ? "rounded-full" : "rounded-[1px]",
            )}
            style={{
              opacity: d === null ? 0.07 : 0.15,
              animation:
                d === null
                  ? "none"
                  : `su-pixel-on ${dur}ms ease-in-out ${d}ms infinite`,
            }}
          />
        ))}
      </span>

      <span
        className="bg-clip-text text-[13px] font-medium text-transparent motion-reduce:!animate-none motion-reduce:!text-current"
        style={{
          backgroundImage:
            "linear-gradient(90deg, #a3a3a3 35%, currentColor 50%, #a3a3a3 65%)",
          backgroundSize: "200% 100%",
          animation: "su-shimmer-text 1.4s linear infinite",
        }}
      >
        {label}
      </span>

      <span
        role="timer"
        aria-label={`Elapsed time ${elapsed}`}
        className="font-mono text-[12px] tabular-nums text-neutral-400 dark:text-neutral-500"
      >
        {elapsed}
      </span>
    </div>
  );
}

export default LoadingState;
```
