I built a React skeleton loader that traces your UI automatically. Meet shimmer-trace.
Skeleton loaders are everywhere. Every modern app has them. But if you have ever built one by hand, you know the pain:
- You copy your real card, replace text with grey boxes.
- Then your designer changes the padding, and your skeleton looks wrong again.
- Each grey box has its own shimmer animation, and they all run out of sync, like a broken disco floor.
- You ship it. Layout shifts. Lighthouse cries.
I got tired of this. So I built shimmer-trace — a React library that looks at your real UI, traces the shape of every element, and paints one single shimmer wave across the whole thing.
One wrapper. Zero hand-drawn skeletons. Zero layout shift.
npm install shimmer-trace
The one-line demo
import { Shimmer } from 'shimmer-trace';
<Shimmer loading={isLoading}>
<UserCard user={user} />
</Shimmer>
That is it. No <SkeletonCard />. No fake grey divs. The library reads the real <UserCard>, measures every text node and image, and draws skeleton blocks in the exact same shape.
When loading flips to false, the shimmer disappears and your real UI is already there — same size, same position. No jump.
Why it feels different
Most skeleton libraries give you grey blocks that you place by hand. Each block animates on its own. So when you have ten cards on screen, you get ten separate shimmer waves all running at slightly different times. It looks busy and cheap.
shimmer-trace does the opposite. It collects every block into one overlay and runs one wave across the whole page. Your eye follows a single line of light. It feels calm. It feels premium.
How it works inside
There are four ideas. Once you see them, the whole library makes sense.
1. The Ghost Overlay
When loading, the library renders your real children with visibility: hidden. The DOM is still there. The browser still does layout. Buttons still take up space. But nothing is painted.
This means zero CLS (Cumulative Layout Shift). The skeleton and the real UI occupy the exact same pixels.
2. Tracing the DOM
A small hook called useTrace walks the container with getBoundingClientRect, grabs the position and border-radius of every leaf element, and stores them as a list of rectangles.
A ResizeObserver re-runs the trace when anything moves. So if the user resizes the window, the shimmer follows.
3. The Bubbling Registry
Here is the smart part. You can nest <Shimmer> components. The library uses React Context so that nested shimmers do not each draw their own overlay. Instead, child shimmers report their measured rectangles up to the nearest parent shimmer.
The master gets a full map of every rectangle in the tree. It draws one overlay. One wave. Whole page.
4. The CSS Mask
The overlay is a single absolutely positioned <div> that covers the whole master container. It has a moving gradient background — that is the "shimmer." On top, a CSS mask-image made of all the collected rectangles cuts the gradient into the right shape.
overlay div = [ moving gradient ] × [ mask of N rectangles ]
↑ ↑
one animation shape of your UI
One animation. Many shapes. Perfect sync.
Combined working Diagram
A bigger example
import { createShimmer } from 'shimmer-trace';
const AppShimmer = createShimmer({
animation: 'wave',
baseColor: '#1a1a2e',
highlightColor: '#2a2a4e',
speed: 1.5,
});
function FruitList({ fruits, loading }) {
return (
<AppShimmer
loading={loading}
dummyLength={5}
dummyData={{ fruit: { name: 'xxxxxxx', price: '$0.00' } }}
>
{fruits.map((f) => <FruitCard key={f.id} fruit={f} />)}
</AppShimmer>
);
}
Two things happen here:
-
createShimmerbakes your theme into a component, so you do not pass colors everywhere. -
dummyLength+dummyDatalet the library render fake cards before any real data arrives. No moredata?.map(...)and no manual placeholders.
What you get out of the box
- Zero CLS. Real DOM measures the layout.
- One synchronized wave across the whole tree.
- Three animations: wave, pulse, breathe.
-
Auto resize.
ResizeObserverre-traces on layout change. -
A11y baked in.
aria-busy="true"androle="status"on the overlay. - Zero dependencies. Tiny bundle. Just React.
- TypeScript first.
Why I made it
I wanted skeleton loaders to stop being a second design pass. You should not have to draw your loading state by hand. Your real component already knows its shape. The library should just read it.
If you have ever spent an afternoon nudging grey rectangles to match a card, this is for you.
Try it
npm install shimmer-trace
- GitHub: github.com/jeetvora331/shimmer-trace
- NPM: shimmer-trace
- Star it, break it, open issues. I want to make it better.
If this saved you even one hour of skeleton work, share it with one friend who still hand-codes their loaders. They will thank you.
Built with React 18+, zero dependencies, MIT licensed.




Top comments (0)