Skeleton
A hand-drawn loading placeholder that animates its hachure shading, styled with shadcn conventions.
Installation
npx shadcn@latest add https://sketchcn.chuwii.com/r/skeleton.jsonStyles
Skeleton leans on these rules from sketch.css. The shadcn CLI installs the file with sketch-provider; copying the component by hand means copying them too.
@property --sketch-bg-reveal-lead {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
@property --sketch-bg-reveal-trail {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
[data-sketch-reveal] [data-sketch-bg] {
--sketch-bg-reveal-mask: linear-gradient(
calc((180 + var(--sketch-bg-hachure-angle)) * 1deg),
transparent var(--sketch-bg-reveal-trail),
#000 var(--sketch-bg-reveal-trail),
#000 var(--sketch-bg-reveal-lead),
transparent var(--sketch-bg-reveal-lead)
);
mask-image: var(--sketch-bg-reveal-mask);
-webkit-mask-image: var(--sketch-bg-reveal-mask);
animation: sketch-hatch-reveal 2400ms linear infinite;
}
@keyframes sketch-hatch-reveal {
0% {
--sketch-bg-reveal-lead: 0%;
--sketch-bg-reveal-trail: 0%;
}
45%,
55% {
--sketch-bg-reveal-lead: 100%;
--sketch-bg-reveal-trail: 0%;
}
100% {
--sketch-bg-reveal-lead: 100%;
--sketch-bg-reveal-trail: 100%;
}
}Read the whole file for the drawing variables, paper textures and reduced-motion rules.
Usage
import { Skeleton } from "@/components/ui/skeleton";
export function LoadingLines() {
return (
<div className="flex w-44 flex-col gap-2">
<Skeleton className="h-4 w-2/3" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-1/2" />
</div>
);
}Props
Takes every div prop. Size it with utility classes; there is no size variant.
Examples
Profile loading
import { Skeleton } from "@/components/ui/skeleton";
export function ProfileLoading() {
return (
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-full" />
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-20" />
</div>
</div>
);
}List loading
import { Skeleton } from "@/components/ui/skeleton";
export function NoteListLoading() {
return (
<ul className="flex w-full flex-col gap-3">
{[0, 1, 2].map((row) => (
<li key={row} className="flex flex-col gap-2">
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-3 w-full" />
</li>
))}
</ul>
);
}Wider hatching
import { Skeleton } from "@/components/ui/skeleton";
export function CalmSkeleton() {
return <Skeleton className="h-24 w-full [--sketch-bg-hachure-gap:8]" />;
}Upright hatching
import { Skeleton } from "@/components/ui/skeleton";
export function UprightSkeleton() {
return (
<Skeleton className="h-24 w-full [--sketch-bg-hachure-angle:90]" />
);
}Animation
Skeleton is a data-sketch-reveal wrapper around a data-sketch-bg svg. sketch.css masks the hachure and wipes it on a 2400ms sketch-hatch-reveal loop; there is no JavaScript timer.
[data-sketch-reveal] [data-sketch-bg] {
--sketch-bg-reveal-mask: linear-gradient(
calc((180 + var(--sketch-bg-hachure-angle)) * 1deg),
transparent var(--sketch-bg-reveal-trail),
#000 var(--sketch-bg-reveal-trail),
#000 var(--sketch-bg-reveal-lead),
transparent var(--sketch-bg-reveal-lead)
);
mask-image: var(--sketch-bg-reveal-mask);
-webkit-mask-image: var(--sketch-bg-reveal-mask);
animation: sketch-hatch-reveal 2400ms linear infinite;
}The mask angle is derived from --sketch-bg-hachure-angle, so rotating the hatch rotates the wipe with it. Under prefers-reduced-motion the mask and the animation are both dropped, leaving a static hatched block.