Shimmer Skeleton
Loading skeleton with a soft diagonal shimmer wave sweeping across, in 4 composable variants
skeletonloadershimmerplaceholder
Preview
Source Code
"use client";
const SHIMMER_STYLE = `
@keyframes shimmer-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
`;
export interface ShimmerSkeletonProps {
/** Variant. Default: 'line' */
variant?: "line" | "block" | "avatar" | "card";
/** Width (CSS). Defaults per variant. */
width?: string | number;
/** Height (CSS). Defaults per variant. */
height?: string | number;
/** Shimmer duration in seconds. Default: 1.8 */
duration?: number;
/** Base bg color. Default: 'rgba(255,255,255,0.06)' */
baseColor?: string;
/** Highlight color. Default: 'rgba(255,255,255,0.12)' */
highlightColor?: string;
className?: string;
}
type VariantConfig = {
width: string;
height: string;
borderRadius: string;
};
const VARIANT_DEFAULTS: Record<string, VariantConfig> = {
line: { width: "100%", height: "14px", borderRadius: "4px" },
block: { width: "100%", height: "120px", borderRadius: "12px" },
avatar: { width: "48px", height: "48px", borderRadius: "9999px" },
card: { width: "320px", height: "180px", borderRadius: "16px" },
};
function toCss(value: string | number): string {
return typeof value === "number" ? `${value}px` : value;
}
function SkeletonBase({
width,
height,
borderRadius,
duration,
baseColor,
highlightColor,
className = "",
}: {
width: string;
height: string;
borderRadius: string;
duration: number;
baseColor: string;
highlightColor: string;
className?: string;
}) {
return (
<div
className={className}
style={{
position: "relative",
overflow: "hidden",
width,
height,
borderRadius,
background: baseColor,
flexShrink: 0,
}}
>
<div
style={{
position: "absolute",
inset: 0,
background: `linear-gradient(110deg, transparent 30%, ${highlightColor} 50%, transparent 70%)`,
animation: `shimmer-sweep ${duration}s ease-in-out infinite`,
}}
/>
</div>
);
}
export default function ShimmerSkeleton({
variant = "line",
width,
height,
duration = 1.8,
baseColor = "rgba(255,255,255,0.06)",
highlightColor = "rgba(255,255,255,0.12)",
className = "",
}: ShimmerSkeletonProps) {
const defaults = VARIANT_DEFAULTS[variant];
const resolvedWidth = width != null ? toCss(width) : defaults.width;
const resolvedHeight = height != null ? toCss(height) : defaults.height;
const borderRadius = defaults.borderRadius;
if (variant === "card") {
return (
<>
<style dangerouslySetInnerHTML={{ __html: SHIMMER_STYLE }} />
<div
className={className}
style={{
position: "relative",
overflow: "hidden",
width: resolvedWidth,
height: resolvedHeight,
borderRadius,
background: baseColor,
flexShrink: 0,
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
padding: "16px",
gap: "8px",
}}
>
{/* Global shimmer sweep on the card wrapper */}
<div
style={{
position: "absolute",
inset: 0,
background: `linear-gradient(110deg, transparent 30%, ${highlightColor} 50%, transparent 70%)`,
animation: `shimmer-sweep ${duration}s ease-in-out infinite`,
}}
/>
{/* Inner lines — title + 2 body lines */}
<div style={{ position: "relative", zIndex: 1, display: "flex", flexDirection: "column", gap: "8px" }}>
<div
style={{
width: "60%",
height: "14px",
borderRadius: "4px",
background: highlightColor,
}}
/>
<div
style={{
width: "90%",
height: "12px",
borderRadius: "4px",
background: highlightColor,
}}
/>
<div
style={{
width: "75%",
height: "12px",
borderRadius: "4px",
background: highlightColor,
}}
/>
</div>
</div>
</>
);
}
return (
<>
<style dangerouslySetInnerHTML={{ __html: SHIMMER_STYLE }} />
<SkeletonBase
width={resolvedWidth}
height={resolvedHeight}
borderRadius={borderRadius}
duration={duration}
baseColor={baseColor}
highlightColor={highlightColor}
className={className}
/>
</>
);
}