Skeleton Reveal
A loading skeleton built from the real content — so it's sized exactly right and nothing shifts on load — that wipes the content in with an animated mask. Pure CSS.
Sean Geng
@seangeng
Co-founder & CTO at B3. Building a crypto agent & decentralized inference.
Usage
import { SkeletonReveal } from "~/components/skeleton/skeleton-reveal";// Controlled: flip `loading` false when your data arrives.<SkeletonReveal loading={isLoading}><ProfileCard user={user} /></SkeletonReveal>// Or scrub it by hand (0 = skeleton, 1 = revealed).<SkeletonReveal progress={0.5} direction="left"><ProfileCard user={user} /></SkeletonReveal>
How it avoids layout shift
The usual skeleton is a separate set of grey boxes you size by hand to approximately match the real thing — then the content loads, the sizes don't quite agree, and the page jumps. This one keeps the real content mounted the whole time. A skeleton layer (the same content, with its leaf elements and anything marked data-skel turned into shimmering blocks) sits behind the real content. Because it's the actual content, the skeleton is the exact final size — so when it reveals, nothing shifts (zero CLS).
The reveal is an animated mask-image: a soft gradient slides across the content, wiping it in over the skeleton. Drive it with a timer, with a loading flag, or — as in the demo — scrub it by hand with a single --reveal-progress variable. The Motion example drives the same mask through the browser's View Transition API; here it's plain CSS, so there's no dependency and it degrades cleanly under prefers-reduced-motion.
/* Two layers in one grid cell: skeleton behind, real content on top. */.reveal { display: grid; }.reveal > * { grid-area: 1 / 1; }/* Skeleton layer: leaf elements become shimmering blocks (exact sizes). */.reveal__skeleton :where(h1,h2,h3,p,span,a,button,[data-skel]) {color: transparent !important;background-color: hsl(var(--muted)) !important;background-image: linear-gradient(90deg, transparent,hsl(var(--foreground)/.07) 50%, transparent) !important;background-size: 220% 100% !important;border-radius: 8px !important;animation: reveal-shimmer 1.3s ease-in-out infinite;}/* Content layer: a soft gradient mask, driven by --reveal-progress. */.reveal--scrub .reveal__content {mask-image: linear-gradient(90deg, #000 0% 35%, transparent 70%);mask-size: 300% 100%;mask-repeat: no-repeat;mask-position: calc((1 - var(--reveal-progress)) * 100%) 0;}.reveal--scrub .reveal__skeleton { opacity: calc(1 - var(--reveal-progress)); }
Props
| prop | type | default | description |
|---|---|---|---|
| children | ReactNode | — | The real content. Stays mounted — that's what makes the skeleton exact. |
| loading | boolean | — | Controlled flag; true → false wipes content in. |
| progress | number | — | Manual scrub, 0..1 (0 skeleton, 1 revealed). Disables timing. |
| direction | "left" | "right" | "left" | Wipe direction. |
| angle | number | 8 | Tilt of the wipe edge in degrees (0 = vertical). |
| delay | number | 1400 | Uncontrolled: ms to hold the skeleton before revealing. |
| duration | number | 700 | Reveal duration in ms (auto/controlled mode). |