d2
uipixel-spinnersource: ok · hit
Pixel Spinner
The pixel-grid spinner engine every spinner preset builds on.
npx shadcn add "https://ui.d2studio.dev/r/pixel-spinner.json"free
Visual style
retro
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Files2 · source cached 10/8/2026, 7:40:20 PM
registry/default/ui/pixel-spinner.tsx
"use client";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import "./pixel-spinner.css";
export type SpinnerColor = "crimson" | "hotpink" | "violet" | "blue";
export type SpinnerPattern = {
size?: number;
rows?: number;
cols?: number;
frames: number[][];
interval?: number;
};
export type SpinnerGradient = { from: string; to: string; glow: string };
export type SpinnerShape =
| "square"
| "rounded"
| "circle"
| "diamond"
| "triangle"
| "lines"
| "line-2"
| "line-3";
export type SpinnerEffect = "none" | "light" | "wave";
export type SpinnerAnimation = "pixels" | "wavy";
const SHAPE_STYLE: Record<SpinnerShape, React.CSSProperties> = {
square: {},
rounded: { borderRadius: "22%" },
circle: { borderRadius: "50%" },
diamond: { clipPath: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)" },
triangle: { clipPath: "polygon(50% 0, 100% 100%, 0 100%)" },
lines: {
clipPath: "inset(calc(100% - 0.2px) 0 0 0)",
},
"line-2": {
maskImage:
"repeating-linear-gradient(to bottom, black 0 1px, transparent 1px 3px)",
WebkitMaskImage:
"repeating-linear-gradient(to bottom, black 0 1px, transparent 1px 3px)",
},
"line-3": {},
};
interface PixelSpinnerProps {
pattern: SpinnerPattern;
color: SpinnerColor;
cellSize?: number;
gap?: number;
className?: string;
customColor?: string;
gradient?: SpinnerGradient;
intervalOverride?: number;
glow?: number;
glowSpread?: number;
easeIn?: number;
easeOut?: number;
shape?: SpinnerShape;
effect?: SpinnerEffect;
animation?: SpinnerAnimation;
/** When true, every cell entering the peak set runs an elastic
* scale + brightness bounce via a CSS keyframe. */
pop?: boolean;
/** Intensity multiplier for the pop bounce (0 = no movement, 1 = default,
* up to ~2 for an exaggerated punch). */
popStrength?: number;
/** Duration of one pop cycle in ms. */
popDuration?: number;
}
export function PixelSpinner({
pattern,
color,
cellSize = 14,
gap = 2,
className,
customColor,
gradient,
intervalOverride,
glow,
glowSpread,
easeIn,
easeOut,
shape = "square",
effect = "light",
animation = "pixels",
pop = false,
popStrength = 1,
popDuration = 480,
}: PixelSpinnerProps) {
const shapeStyle = SHAPE_STYLE[shape];
const cols = pattern.cols ?? pattern.size ?? 3;
const rows = pattern.rows ?? pattern.size ?? 3;
const interval = intervalOverride ?? pattern.interval ?? 220;
const [frame, setFrame] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setFrame((f) => (f + 1) % pattern.frames.length);
}, interval);
return () => clearInterval(id);
}, [pattern.frames.length, interval]);
const trailOpacities = [1, 0.5, 0.25, 0.15];
const cellOpacity = new Map<number, number>();
for (let t = 0; t < trailOpacities.length; t++) {
const f = (frame - t + pattern.frames.length) % pattern.frames.length;
const cells = pattern.frames[f] ?? [];
for (const c of cells) {
if (!cellOpacity.has(c)) {
cellOpacity.set(c, trailOpacities[t]);
}
}
}
const total = rows * cols;
return (
<div className="luminous-spinners inline-block">
<div
className={cn(
"spinner-grid grid",
effect === "light" && "effect-light",
effect === "wave" && "effect-wave",
animation === "wavy" ? "anim-wavy" : "anim-pixels",
pop && "pop-on-peak",
className,
)}
style={
{
gridTemplateColumns: `repeat(${cols}, ${cellSize}px)`,
gap: `${gap}px`,
...(glow !== undefined ? { ["--glow"]: String(glow) } : {}),
...(glowSpread !== undefined
? { ["--glow-spread"]: `${glowSpread}px` }
: {}),
...(easeIn !== undefined ? { ["--ease-in"]: `${easeIn}ms` } : {}),
...(easeOut !== undefined
? { ["--ease-out"]: `${easeOut}ms` }
: {}),
...(pop
? {
["--ls-pop-strength"]: String(popStrength),
["--ls-pop-duration"]: `${popDuration}ms`,
}
: {}),
} as React.CSSProperties
}
role="status"
aria-label="Loading"
>
{Array.from({ length: total }).map((_, i) => {
const op = cellOpacity.get(i);
const useCustom = !!customColor || !!gradient;
const variant = useCustom ? "c-custom" : `c-${color}`;
const customVars: Record<string, string> = {};
if (gradient) {
customVars["--cell-gradient"] =
`linear-gradient(135deg, ${gradient.from}, ${gradient.to})`;
customVars["--cell-glow"] = gradient.glow;
} else if (customColor) {
customVars["--cell-color"] = customColor;
}
return (
<div
key={i}
className={cn(
"cell",
`shape-${shape}`,
op !== undefined && `on ${variant}`,
op === 1 && "peak",
)}
style={
{
width: cellSize,
height: cellSize,
...shapeStyle,
...customVars,
...(op !== undefined ? { opacity: op } : {}),
} as React.CSSProperties
}
/>
);
})}
</div>
</div>
);
}
registry/default/ui/pixel-spinner.css
/* Pixel Spinner — scoped styles. Imported by pixel-spinner.tsx. */
.luminous-spinners {
/* Light theme defaults */
--ls-foreground: var(--foreground);
--ls-muted-foreground: var(--muted-foreground);
--ls-card: var(--card);
--ls-border: var(--border);
--ls-code-bg: var(--muted);
--ls-cell-off: radial-gradient(
circle at 35% 30%,
oklch(0 0 0 / 0.08),
oklch(0 0 0 / 0.02) 60%,
transparent 100%
);
--ls-cell-stroke: oklch(0 0 0 / 0.08);
--ls-crimson: oklch(0.75 0.22 145);
--ls-hotpink: oklch(0.72 0.24 5);
--ls-violet: oklch(0.7 0.22 305);
--ls-blue: oklch(0.7 0.2 255);
--ls-gradient-crimson: linear-gradient(135deg, oklch(0.85 0.18 145), oklch(0.65 0.24 140));
--ls-gradient-hotpink: linear-gradient(135deg, oklch(0.85 0.2 350), oklch(0.65 0.25 10));
--ls-gradient-violet: linear-gradient(135deg, oklch(0.85 0.18 320), oklch(0.6 0.25 285));
--ls-gradient-blue: linear-gradient(135deg, oklch(0.85 0.16 240), oklch(0.55 0.24 265));
}
.dark .luminous-spinners {
--ls-cell-off: radial-gradient(
circle at 35% 30%,
oklch(1 0 0 / 0.06),
oklch(1 0 0 / 0.02) 60%,
transparent 100%
);
--ls-cell-stroke: oklch(1 0 0 / 0.05);
}
.luminous-spinners .cell {
position: relative;
border-radius: 0;
background: var(--ls-cell-off);
box-shadow: inset 0 0 0 1px var(--ls-cell-stroke);
transition:
background var(--ease-out, 600ms) ease-out,
transform var(--ease-out, 600ms) ease-out,
box-shadow var(--ease-out, 600ms) ease-out,
opacity var(--ease-out, 600ms) ease-out;
}
.luminous-spinners .cell.on {
transform: scale(1.08);
transition:
background var(--ease-in, 300ms) ease-in,
transform var(--ease-in, 300ms) ease-in,
box-shadow var(--ease-in, 300ms) ease-in,
opacity var(--ease-in, 300ms) ease-in;
}
.luminous-spinners .anim-wavy .cell {
transition:
background 320ms cubic-bezier(0.34, 1.56, 0.64, 1),
transform 320ms cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 320ms cubic-bezier(0.34, 1.56, 0.64, 1),
opacity 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.luminous-spinners .anim-wavy .cell.on {
transform: scale(1.35);
transition:
background 320ms cubic-bezier(0.34, 1.56, 0.64, 1),
transform 320ms cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 320ms cubic-bezier(0.34, 1.56, 0.64, 1),
opacity 320ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes ls-pop-on-peak {
0% {
transform: scale(1);
filter: brightness(1) saturate(1);
}
22% {
transform: scale(calc(1 - 0.3 * var(--ls-pop-strength, 1)));
filter: brightness(calc(1 + 0.35 * var(--ls-pop-strength, 1)))
saturate(calc(1 + 0.25 * var(--ls-pop-strength, 1)));
}
48% {
transform: scale(calc(1 + 0.18 * var(--ls-pop-strength, 1)));
filter: brightness(calc(1 + 0.2 * var(--ls-pop-strength, 1)))
saturate(calc(1 + 0.15 * var(--ls-pop-strength, 1)));
}
72% {
transform: scale(calc(1 - 0.04 * var(--ls-pop-strength, 1)));
}
100% {
transform: scale(1);
filter: brightness(1) saturate(1);
}
}
.luminous-spinners .pop-on-peak .cell.on.peak {
animation: ls-pop-on-peak var(--ls-pop-duration, 480ms)
cubic-bezier(0.34, 1.56, 0.64, 1);
}
.luminous-spinners .cell.on.c-crimson {
background: var(--ls-gradient-crimson);
box-shadow:
0 0 calc(8px * var(--glow, 1)) var(--glow-spread, 0px) oklch(0.75 0.22 145 / 0.95),
0 0 calc(22px * var(--glow, 1)) oklch(0.7 0.24 140 / 0.75),
0 0 calc(44px * var(--glow, 1)) oklch(0.65 0.26 135 / 0.55),
0 0 calc(72px * var(--glow, 1)) oklch(0.65 0.26 135 / 0.35);
}
.luminous-spinners .cell.on.c-hotpink {
background: var(--ls-gradient-hotpink);
box-shadow:
0 0 calc(8px * var(--glow, 1)) var(--glow-spread, 0px) oklch(0.72 0.24 5 / 0.95),
0 0 calc(22px * var(--glow, 1)) oklch(0.7 0.25 10 / 0.75),
0 0 calc(44px * var(--glow, 1)) oklch(0.65 0.25 15 / 0.55),
0 0 calc(72px * var(--glow, 1)) oklch(0.65 0.25 15 / 0.35);
}
.luminous-spinners .cell.on.c-violet {
background: var(--ls-gradient-violet);
box-shadow:
0 0 calc(8px * var(--glow, 1)) var(--glow-spread, 0px) oklch(0.7 0.22 305 / 0.95),
0 0 calc(22px * var(--glow, 1)) oklch(0.65 0.24 300 / 0.75),
0 0 calc(44px * var(--glow, 1)) oklch(0.6 0.25 295 / 0.55),
0 0 calc(72px * var(--glow, 1)) oklch(0.6 0.25 295 / 0.35);
}
.luminous-spinners .cell.on.c-blue {
background: var(--ls-gradient-blue);
box-shadow:
0 0 calc(8px * var(--glow, 1)) var(--glow-spread, 0px) oklch(0.7 0.2 255 / 0.95),
0 0 calc(22px * var(--glow, 1)) oklch(0.6 0.22 260 / 0.75),
0 0 calc(44px * var(--glow, 1)) oklch(0.55 0.24 265 / 0.55),
0 0 calc(72px * var(--glow, 1)) oklch(0.55 0.24 265 / 0.35);
}
.luminous-spinners .cell.on.c-custom {
background: var(
--cell-gradient,
linear-gradient(
135deg,
color-mix(in oklab, var(--cell-color, #7ab7ff) 80%, white),
color-mix(in oklab, var(--cell-color, #7ab7ff) 85%, black)
)
);
box-shadow:
0 0 calc(8px * var(--glow, 1)) var(--glow-spread, 0px)
color-mix(
in oklab,
var(--cell-glow, var(--cell-color, #7ab7ff)) 95%,
transparent
),
0 0 calc(22px * var(--glow, 1))
color-mix(
in oklab,
var(--cell-glow, var(--cell-color, #7ab7ff)) 75%,
transparent
),
0 0 calc(44px * var(--glow, 1))
color-mix(
in oklab,
var(--cell-glow, var(--cell-color, #7ab7ff)) 55%,
transparent
),
0 0 calc(72px * var(--glow, 1))
color-mix(
in oklab,
var(--cell-glow, var(--cell-color, #7ab7ff)) 35%,
transparent
);
}
.luminous-spinners .cell.shape-line-3::before {
content: "";
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(
to bottom,
rgba(69, 69, 69, 0.75) 0 11%,
transparent 11% 22%
);
pointer-events: none;
mix-blend-mode: multiply;
border-radius: inherit;
}
.luminous-spinners .effect-light .cell.on::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
115deg,
transparent 30%,
oklch(1 0 0 / 0.45) 50%,
transparent 70%
);
background-size: 250% 100%;
animation: ls-shimmer 1.8s linear infinite;
mix-blend-mode: overlay;
pointer-events: none;
}
@keyframes ls-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.luminous-spinners .spinner-grid {
position: relative;
}
.luminous-spinners .spinner-grid.effect-wave {
overflow: hidden;
}
.luminous-spinners .spinner-grid.effect-wave::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
115deg,
transparent 35%,
oklch(1 0 0 / 0.35) 50%,
transparent 65%
);
background-size: 260% 260%;
animation: ls-wave 2.8s linear infinite;
mix-blend-mode: screen;
pointer-events: none;
z-index: 3;
}
@keyframes ls-wave {
0% {
background-position: 140% 140%;
}
100% {
background-position: -140% -140%;
}
}