Loading State
Pixel-grid loader with shimmer and elapsed time.
"use client";
/*
* MIT License
*
* Copyright (c) 2026 Shane Levine
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import "./styles/foundation.css";
import { useEffect, useState } from "react";
/* ─────────────────────────────────────────────────────────
* LOADING STATE — pixel-grid loader for long-running work
*
* Variants:
* Drive — square cells, chevron wavefront driving right;
* the 650ms cycle is shorter than the sweep, so
* two fronts are always in flight
* Dots — same wavefront, circular cells
* Orbit — a comet lapping the grid perimeter
* Surfer — the Drive loader paired with a meme video below
*
* Paired with a shimmering label and a live elapsed timer
* in mono tabular figures. Reduced motion freezes the grid
* to its dim state; the timer still ticks.
* ───────────────────────────────────────────────────────── */
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;
});
const PATTERNS: Record<string, { 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 LoaderGrid({
delays,
dur,
round,
}: {
delays: (number | null)[];
dur: number;
round: boolean;
}) {
return (
<span aria-hidden className="grid shrink-0 grid-cols-[repeat(3,4px)] gap-[1.5px]">
{delays.map((delay, index) => (
<span
key={index}
className={`size-[4px] bg-ink ${round ? "rounded-full" : "rounded-[1px]"}`}
style={{
opacity: delay === null ? 0.07 : 0.15,
animation: delay === null ? "none" : `pixel-on ${dur}ms ease-in-out ${delay}ms infinite`,
}}
/>
))}
</span>
);
}
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 default function LoadingState({
label,
variant = "Drive",
videoSrc,
}: {
label?: string;
variant?: string;
videoSrc?: string;
}) {
const elapsed = useElapsed();
const surfer = variant === "Surfer";
const resolvedLabel = label ?? (surfer ? "Background task" : "Working");
const { delays, dur, round } = PATTERNS[variant] ?? PATTERNS.Drive;
const labelEl = (
<span
className="bg-clip-text text-[13px] font-medium text-transparent"
style={{
backgroundImage:
"linear-gradient(90deg, var(--ink-3) 35%, var(--ink) 50%, var(--ink-3) 65%)",
backgroundSize: "200% 100%",
animation: "shimmer-text 1.4s linear infinite",
}}
>
{resolvedLabel}
</span>
);
const elapsedEl = <span className="font-mono text-[12px] text-ink-3 tabular-nums">{elapsed}</span>;
if (surfer) {
return (
<div role="status" className="flex w-fit flex-col items-start">
<div className="flex items-center gap-2.5">
<LoaderGrid {...PATTERNS.Drive} />
{labelEl}
{elapsedEl}
</div>
{/* the context card follows the status text it is illustrating */}
<div
className="mt-2 w-56 overflow-hidden rounded-[10px] shadow-overlay"
style={{ animation: "pop-in 200ms cubic-bezier(0.16,1,0.3,1) both", transformOrigin: "top left" }}
>
<div className="relative aspect-video w-full" style={{ background: "var(--tooltip-bg)" }}>
{videoSrc ? <video src={videoSrc} controls playsInline preload="none" className="h-full w-full object-cover" /> :
<div className="flex h-full w-full items-center justify-center font-mono text-[12px]" style={{ color: "var(--tooltip-fg)" }}>Background work in progress</div>}
</div>
</div>
</div>
);
}
return (
<div role="status" className="flex w-fit items-center gap-2.5">
<LoaderGrid delays={delays} dur={dur} round={round} />
{labelEl}
{elapsedEl}
</div>
);
}Installation
pnpm dlx shadcn@latest add @ward/loading-statenpx shadcn@latest add @ward/loading-stateyarn dlx shadcn@latest add @ward/loading-statebunx --bun shadcn@latest add @ward/loading-stateAdd the Ward registry to components.json once:
"registries": {
"@ward": "https://ward.so/r/{name}.json"
}Or install from the URL with no configuration: npx shadcn@latest add https://ward.so/r/loading-state.json
Add the Ward items it builds on.
npx shadcn@latest add @ward/foundationCopy each file from the Code tab into:
components/ward/LoadingState.tsx
Update the import paths to match your project.
Usage
import LoadingState from "@/components/ward/LoadingState";<LoadingState variant="Dots" label="Indexing files" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Status text beside the loader; falls back to 'Working', or 'Background task' for Surfer. |
variant | "Drive" | "Dots" | "Orbit" | "Surfer" | "Drive" | Grid style and motion used for the loader, with Surfer adding a video card below it. |
videoSrc | string | - | Video URL played in the Surfer card; without it the card shows a placeholder message. |
Ward · Version 1.1.0 · Registry manifest