A ghostly UI card outline with a shimmer wave sweeping across it, measured by ribbons of light, with a gray-blue cat watching from a windowsill.

Shimmer From Structure: Skeletons That Measure Themselves

The Gray Cat
The Gray Cat
0 views

Every app that loads data has the same awkward moment: the data is in flight, and the screen has nothing to show. The lazy answer is a spinner. The polished answer is a skeleton — a gray ghost of the layout that pulses while content arrives, so the page feels like it is filling in rather than popping into existence. Skeletons are great for perceived performance, but they come with a quiet tax. To build one, you usually hand-craft a second component that mimics the real one block for block, then keep the two in sync forever. Change the avatar size, add a subtitle, tweak a margin, and now you have two places to update instead of one.

Shimmer From Structure deletes that duplicate component entirely. Instead of asking you to describe what the skeleton should look like, it measures what your component actually renders. You wrap your real component in a <Shimmer> element, and at runtime the library reads the live DOM — every leaf element's size, position, and border-radius — and paints shimmer blocks that match exactly. When your layout changes, the skeleton changes with it, because it was never a separate thing to begin with. The shimmer-from-structure package ships adapters for React, Vue, Svelte, Angular, and SolidJS, all built around the same idea.

Why Measuring Beats Mimicking

The traditional skeleton workflow asks you to maintain two versions of every component and re-sync them whenever the layout shifts. That is the exact maintenance burden this library was built to remove. A few things make its approach worth a look:

  • It measures the real component. Using a layout-effect pass, it calls getBoundingClientRect() on each leaf element and builds shimmer blocks from the actual dimensions. No guessing at widths or counting lines by hand.
  • Container chrome stays visible. Rather than hiding content with opacity: 0, it renders your component with color: transparent. Card backgrounds, borders, and padding stay on screen during loading, so the shimmer sits inside a real-looking shell instead of a void.
  • Border-radius is detected automatically. A circular avatar produces a circular shimmer block, because the library reads each element's computed border-radius from CSS. Text nodes, which have no radius, fall back to a configurable rounding so you never get harsh rectangles.
  • One mental model, five frameworks. The same <Shimmer loading={...}> pattern works in React, Vue, Svelte, Angular, and SolidJS.
  • Dark-mode friendly defaults. The default shimmer and background colors are semi-transparent whites that read well on any background.

It is a young project — created at the start of 2026 — but it has gathered north of a thousand GitHub stars and a few thousand weekly downloads quickly, which says something about how much people dislike maintaining skeleton clones.

Getting It Into Your Project

Installation is a single package. The root shimmer-from-structure re-exports the React adapter, so React users can import straight from it.

npm install shimmer-from-structure
yarn add shimmer-from-structure

React imports come from the main package (or the explicit @shimmer-from-structure/react). Other frameworks pull from their scoped adapters: @shimmer-from-structure/vue, @shimmer-from-structure/svelte, @shimmer-from-structure/angular, and @shimmer-from-structure/solid. The examples below lead with React and TypeScript, with a couple of cross-framework looks where it helps.

Wrapping Your First Component

The simplest case is a component with static, hardcoded content. Wrap it in <Shimmer> and flip the loading prop. While loading is true, the library measures the children and renders shimmer blocks; when it flips to false, your real content fades in.

import { useState } from "react";
import { Shimmer } from "shimmer-from-structure";

function UserCard() {
  const [isLoading, setIsLoading] = useState(true);

  return (
    <Shimmer loading={isLoading}>
      <div className="card">
        <img src="avatar.jpg" className="avatar" />
        <h2>John Doe</h2>
        <p>Software Engineer</p>
      </div>
    </Shimmer>
  );
}

There is no second skeleton component here, and no list of widths to keep aligned. The card background, the round avatar, and the two text lines are all measured from the markup you already wrote. If you later add a third line or swap the avatar for a wider banner, the skeleton updates on its own because it is generated from the live DOM.

Feeding Data-Driven Components With templateProps

Static markup is the easy case. Real components usually receive their content through props, and during loading that data does not exist yet — user is null, the list is empty, and there is nothing for the library to measure. This is what templateProps solves. You hand the <Shimmer> a mock object, and it spreads those props onto the first child so the component can render a representative layout purely for measurement.

import { useState } from "react";
import { Shimmer } from "shimmer-from-structure";

interface User {
  name: string;
  role: string;
  avatar: string;
}

const UserCard = ({ user }: { user: User }) => (
  <div className="card">
    <img src={user.avatar} className="avatar" />
    <h2>{user.name}</h2>
    <p>{user.role}</p>
  </div>
);

const userTemplate: User = {
  name: "Loading...",
  role: "Loading role...",
  avatar: "placeholder.jpg",
};

function App() {
  const [loading, setLoading] = useState(true);
  const [user, setUser] = useState<User | null>(null);

  return (
    <Shimmer loading={loading} templateProps={{ user: userTemplate }}>
      <UserCard user={user ?? userTemplate} />
    </Shimmer>
  );
}

The trick is that userTemplate only needs to produce a layout shaped like the real one. The exact text does not matter — it is transparent during loading anyway — but the rough length does, since that is what determines block widths. A good rule of thumb is to make your template strings about as long as typical real values so the skeleton lands close to the final layout.

Tuning The Look

Every <Shimmer> accepts a small set of styling props, so you can match the effect to your brand or vary it per section. The wave color, the block background, the animation duration, and the fallback radius for text are all adjustable.

<Shimmer
  loading={isLoading}
  shimmerColor="rgba(255, 255, 255, 0.2)"
  backgroundColor="rgba(255, 255, 255, 0.1)"
  duration={2}
  fallbackBorderRadius={8}
  templateProps={{ user: userTemplate, settings: settingsTemplate }}
>
  <MyComponent user={user} settings={settings} />
</Shimmer>

The defaults are deliberately neutral: a semi-transparent white wave over a fainter white background, a 1.5-second sweep, and a 4px fallback radius for elements that have no CSS radius of their own. duration is measured in seconds, and fallbackBorderRadius in pixels. Because the defaults lean on translucent whites, they sit comfortably on both light and dark surfaces without extra configuration.

Driving Independent Loading States

Real screens rarely load as one block. A dashboard might have a user header that resolves instantly and a stats grid that takes a moment longer. Because each <Shimmer> owns its own loading flag, you can let sections settle independently, and you can theme them differently to boot.

function Dashboard() {
  const [loadingUser, setLoadingUser] = useState(true);
  const [loadingStats, setLoadingStats] = useState(true);

  return (
    <>
      <Shimmer loading={loadingUser} templateProps={{ user: userTemplate }}>
        <UserProfile user={user} />
      </Shimmer>

      <Shimmer
        loading={loadingStats}
        templateProps={{ stats: statsTemplate }}
        shimmerColor="rgba(20, 184, 166, 0.2)"
      >
        <StatsGrid stats={stats} />
      </Shimmer>
    </>
  );
}

Each region shimmers and resolves on its own schedule. The user header can snap into place while the stats grid keeps pulsing, which mirrors how the data actually arrives and avoids the all-or-nothing feel of a single global spinner.

Pairing With React Suspense

Shimmer slots neatly into React's Suspense as a fallback. The mental model is slightly different here: you set loading={true} and leave it. When the suspended component resolves, React throws the whole fallback away and mounts the real tree, so the Shimmer is unmounted rather than toggled. You never manage the flag yourself.

import { Suspense, lazy, memo } from "react";
import { Shimmer } from "shimmer-from-structure";

const UserProfile = lazy(() => import("./UserProfile"));

const ShimmerFallback = memo(() => (
  <Shimmer loading={true} templateProps={{ user: userTemplate }}>
    <UserProfile />
  </Shimmer>
));

function App() {
  return (
    <Suspense fallback={<ShimmerFallback />}>
      <UserProfile userId="123" />
    </Suspense>
  );
}

Wrapping the fallback in React.memo keeps it from re-rendering needlessly while the boundary is suspended. It is also worth keeping your template data light, since the library measures the DOM synchronously during a layout effect — heavy template logic runs on the critical path to first paint.

Setting Defaults Once With A Provider

Repeating the same color and duration props on every <Shimmer> gets old fast. Each framework offers a provider that sets app-wide (or section-wide) defaults, which individual components can still override. In React and SolidJS it is a <ShimmerProvider>; Vue uses provideShimmerConfig, Svelte uses setShimmerConfig, and Angular wires it through dependency injection with provideShimmerConfig.

import { Shimmer, ShimmerProvider } from "@shimmer-from-structure/react";

function App() {
  return (
    <ShimmerProvider
      config={{
        shimmerColor: "rgba(56, 189, 248, 0.4)",
        backgroundColor: "rgba(56, 189, 248, 0.1)",
        duration: 2.5,
        fallbackBorderRadius: 8,
      }}
    >
      <Dashboard />
    </ShimmerProvider>
  );
}

Inside the provider, a bare <Shimmer loading={true}> inherits the blue theme, and any prop you pass locally wins over the global value:

// Inherits the provider's blue theme
<Shimmer loading={true}><UserCard /></Shimmer>

// Overrides just the duration
<Shimmer loading={true} duration={0.5}><FastCard /></Shimmer>

The same wrap-the-real-component pattern carries across frameworks. In Vue it is <Shimmer :loading="isLoading">, in Svelte <Shimmer loading={isLoading}>, and in Angular <shimmer [loading]="isLoading()"> with the ShimmerComponent import — different syntax, identical behavior underneath.

Where It Fits And What To Weigh

The closest established alternatives are react-loading-skeleton and react-content-loader. The former drops in DOM elements that you still position by hand to model the layout; the latter has you compose skeletons from SVG shapes, which is flexible but fiddly to author. Both are mature and battle-tested, with far larger download counts. What shimmer-from-structure brings is a genuinely different stance: it reads your real component instead of asking you to describe a fake one, and it does so with one consistent API across five frameworks.

That runtime-measurement approach is the whole appeal, and also the place to keep your eyes open. Because it measures the DOM client-side, it is inherently a client-runtime technique — a strong fit for loading states and Suspense, but not something that renders a pixel-perfect skeleton on the server before hydration. Very large component trees and heavy template data add measurement work on the path to first paint, so the README's advice to keep templates lightweight is worth taking seriously. And the quality of a data-driven skeleton is only as good as its templateProps: if your mock produces a one-line layout where real data spans ten, the skeleton will guess small. It is also a young, single-maintainer project, so weigh it accordingly against libraries with millions of weekly installs.

For the common case, though — a React, Vue, Svelte, Angular, or SolidJS app where you are tired of maintaining skeleton clones that drift out of sync — shimmer-from-structure is a refreshing bargain. You write your component once, wrap it, and let the library handle the ghost. The skeleton is never wrong, because it is your actual layout, just rendered transparent and dressed in a moving wave of light.