Skip to content
← components

Skeleton Reveal

new

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

Sean Geng

@seangeng

Co-founder & CTO at B3. Building a crypto agent & decentralized inference.

9
Components
9
Posts
4
Freebies

Usage

example.tsx
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.

skeleton-reveal.css
/* 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

proptypedefaultdescription
childrenReactNodeThe real content. Stays mounted — that's what makes the skeleton exact.
loadingbooleanControlled flag; true → false wipes content in.
progressnumberManual scrub, 0..1 (0 skeleton, 1 revealed). Disables timing.
direction"left" | "right""left"Wipe direction.
anglenumber8Tilt of the wipe edge in degrees (0 = vertical).
delaynumber1400Uncontrolled: ms to hold the skeleton before revealing.
durationnumber700Reveal duration in ms (auto/controlled mode).