Skeleton
Placeholder blocks with a subtle light sweep, text/avatar presets, and a SkeletonReveal container that eases real content in with a gentle rise and un-blur.
Live preview from the library's source
Install
From EasyUI
npx shadcn@latest add Surajmaurya1/easyui/skeletonFrom this collection
npx shadcn@latest add https://allcomps.vercel.app/r/easyui--skeleton.jsonCode
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "framer-motion";
import { cn } from "../../lib/utils";
export interface SkeletonProps extends React.ComponentProps<"div"> {}
/** A quiet block with a slow light sweep. Size it with className. */
export function Skeleton({ className, ...props }: SkeletonProps) {
return (
<div
aria-hidden
className={cn("relative overflow-hidden rounded-md bg-neutral-200/70 dark:bg-neutral-800/70", className)}
{...props}
>
<motion.span
className="absolute inset-0 bg-[linear-gradient(90deg,transparent,var(--sk),transparent)] [--sk:rgb(255_255_255/0.65)] dark:[--sk:rgb(255_255_255/0.07)]"
initial={{ x: "-100%" }}
animate={{ x: "100%" }}
transition={{ duration: 1.6, ease: "easeInOut", repeat: Infinity, repeatDelay: 0.2 }}
/>
</div>
);
}
const WIDTHS = ["100%", "94%", "98%", "88%"];
export interface SkeletonTextProps {
lines?: number;
className?: string;
}
export function SkeletonText({
lines = 3,
className,
}: SkeletonTextProps) {
return (
<div aria-hidden className={cn("space-y-2.5", className)}>
{Array.from({ length: lines }, (_, i) => (
<Skeleton
key={i}
className="h-3.5"
style={{ width: i === lines - 1 && lines > 1 ? "62%" : WIDTHS[i % WIDTHS.length] }}
/>
))}
</div>
);
}
export interface SkeletonAvatarProps {
size?: number;
className?: string;
}
export function SkeletonAvatar({ size = 40, className }: SkeletonAvatarProps) {
return <Skeleton className={cn("shrink-0 rounded-full", className)} style={{ width: size, height: size }} />;
}
export interface SkeletonRevealProps {
loading: boolean;
skeleton: React.ReactNode;
children: React.ReactNode;
className?: string;
}
/**
* Swap a skeleton for real content. Content eases in (fade, rise, un-blur).
* Make the skeleton roughly the same size as the content so nothing jumps.
*/
export function SkeletonReveal({
loading,
skeleton,
children,
className,
}: SkeletonRevealProps) {
return (
<div className={className}>
<AnimatePresence mode="wait" initial={false}>
{loading ? (
<motion.div
key="skeleton"
role="status"
aria-busy="true"
aria-label="Loading"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.12 }}
>
{skeleton}
</motion.div>
) : (
<motion.div
key="content"
initial={{ opacity: 0, y: 6, filter: "blur(4px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{ duration: 0.3, ease: [0.32, 0.72, 0, 1] }}
>
{children}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Other loaders & skeletons
The same kind of component from the other libraries in the collection.