DEV Community

Cover image for Stop hand-drawing skeletons. Let your UI trace itself magically.
jeetvora331
jeetvora331

Posted on

Stop hand-drawing skeletons. Let your UI trace itself magically.

I built a React skeleton loader that traces your UI automatically. Meet shimmer-trace.

🚀 Live Demo

shimmer-trace demo

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
Enter fullscreen mode Exit fullscreen mode

The one-line demo

import { Shimmer } from 'shimmer-trace';

<Shimmer loading={isLoading}>
  <UserCard user={user} />
</Shimmer>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Two things happen here:

  • createShimmer bakes your theme into a component, so you do not pass colors everywhere.
  • dummyLength + dummyData let the library render fake cards before any real data arrives. No more data?.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. ResizeObserver re-traces on layout change.
  • A11y baked in. aria-busy="true" and role="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
Enter fullscreen mode Exit fullscreen mode

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)