/**
* Skeleton (web). Shimmer placeholder that holds layout while content
* loads — a loading state that shows the shape of what's coming reads
* faster than a spinner that shows nothing.
*
*
*
*
*
* The shimmer is a linear-gradient sweep over containerFill, injected as
* a page-level keyframe once (same pattern as Spinner). Reduced motion
* renders a static fill — the placeholder still holds space, it just
* doesn't move.
*/
import { type CSSProperties } from "react";
import { useTheme } from "@plyxui/styles";
import { radius } from "@plyxui/core";
import { useReducedMotion } from "@plyxui/hooks";
export type SkeletonVariant = "text" | "rect" | "circle";
export interface SkeletonProps {
variant?: SkeletonVariant;
width?: number | string;
height?: number | string;
/** For variant="text": number of stacked lines. Last line is shorter. Default 1. */
lines?: number;
className?: string;
style?: CSSProperties;
}
// Inject the keyframes once per page. Avoids a global stylesheet dependency.
const KEYFRAMES_ID = "plyxui-skeleton-keyframes";
function ensureKeyframes() {
if (typeof document === "undefined") return;
if (document.getElementById(KEYFRAMES_ID)) return;
const style = document.createElement("style");
style.id = KEYFRAMES_ID;
style.textContent = `
@keyframes plyxui-shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}`;
document.head.appendChild(style);
}
export function Skeleton({
variant = "rect",
width,
height,
lines = 1,
className,
style,
}: SkeletonProps) {
ensureKeyframes();
const { colors } = useTheme();
const reduced = useReducedMotion();
const base: CSSProperties = reduced
? { background: colors.containerFill }
: {
background: `linear-gradient(90deg, ${colors.containerFill} 25%, ${colors.surfaceFill} 50%, ${colors.containerFill} 75%)`,
backgroundSize: "200% 100%",
animation: "plyxui-shimmer 1.6s linear infinite",
};
if (variant === "text" && lines > 1) {
return (
{Array.from({ length: lines }, (_, i) => (
))}
);
}
const dims: CSSProperties =
variant === "circle"
? {
width: width ?? 40,
height: height ?? width ?? 40,
borderRadius: radius.pill,
}
: variant === "text"
? { width: width ?? "100%", height: height ?? "1em", borderRadius: radius.sm }
: { width: width ?? "100%", height: height ?? 16, borderRadius: radius.md };
return ;
}