npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iskeletor/react

v0.2.5

Published

Automatic skeleton loading screens for React and Next.js, measured from your real components.

Readme

@iskeletor/react

Skeleton loading screens that are measured, not written.

Point it at a component. It renders that component off-screen, asks the browser what the layout came out as, and draws the placeholder from the answer.

import { AutoSkeleton } from "@iskeletor/react";

<AutoSkeleton loading={isLoading}>
  <ProductCard product={product} />
</AutoSkeleton>

That is the whole integration. ProductCard stays untouched and knows nothing about iskeletor.

npm i @iskeletor/react

Documentation and a live demo — switch the component on the page and watch the skeleton be re-measured in your browser.


Why not just write the skeleton

You write h-4 w-32 to stand in for a title. Then the title wraps to two lines, the avatar becomes a circle, someone adds a badge — and the skeleton keeps promising a layout that no longer arrives. Every loading state is a duplicate of the component, maintained by hand, drifting from the day it was written.

Parsing the JSX would not help: it can tell you there is a <div>, not that the div is 338 pixels wide. That number is the cascade, the fonts and the viewport resolved together — and the browser already computes it on every frame. So we ask the browser.

Three situations

The data is already there. Nothing to configure. The component is measured in the background while it is visible, keyed to its props, so the next loading state is exact and instant.

<AutoSkeleton loading={isRefreshing}>
  <ProductCard product={product} />
</AutoSkeleton>

The data has not arrived yet. On a cold load there is no real element to measure. Hand it a sample — the same component with placeholder data — and it measures that instead, so the first paint of a first visit is already exact.

<AutoSkeleton loading={!product} sample={<ProductCard product={PLACEHOLDER} />}>
  {product ? <ProductCard product={product} /> : null}
</AutoSkeleton>

The sample is rendered off-screen through a portal, from inside your tree, so every provider above it still applies. A product card that reads a redux store, the Next router and a locale measures exactly where it stands. (A second React root — the obvious implementation — cannot do this: context does not cross roots, and in a real application that rules out most components.)

A list, where the row count matters. Identical, evenly spaced siblings fold into one node carrying a count and a stride. You never pass a count prop, because the measurement already knows.

<AutoSkeleton loading={loading}>
  <CommentList comments={comments} />   {/* 3 rows in, 3 rows out */}
</AutoSkeleton>

Measuring the real thing instead of a sample

measure="live" walks the children where they already are. Nothing is rendered a second time, so there is no sample to keep in step with the component and no question about context — it is the live tree.

<AutoSkeleton measure="live" id="product-card" loading={loading}>
  <ProductCard product={product} />
</AutoSkeleton>

The trade is timing: a layout only exists once the real content has been on screen, so the first load still shows fallback and every load after it is exact. Pass an id, because the measurement no longer belongs to one set of props — every card in a list shares the entry.

Measurement waits for the content to settle first: images decoded, webfonts loaded, geometry steady for two frames. Without that you measure a card that is still loading its own picture, and the skeleton stands in for the loading state rather than the finished one. Every wait is bounded.

Skeletons already on screen do not miss out. When one part of a page is still fetching while another has just measured the same component, the waiting skeleton picks the layout up as it lands.

Next.js App Router

The package carries its own "use client" boundary, so it imports straight into a server component — in Next.js the boundary is declared by the file being imported, not the file importing it.

// app/page.tsx — a server component, no "use client" needed
import { AutoSkeleton } from "@iskeletor/react";

export default function Page() {
  return <AutoSkeleton loading sample={<Card data={SAMPLE} />} fallback={<Box />} />;
}

Hydration stays clean by construction. The server has no layout engine, so it cannot measure anything. Server render and first client render both produce fallback — identical trees — and measuring only begins in an effect, after hydration has already agreed.

Verified against Next 15.5 and Next 16.3 with React 19.

Props

| Prop | Default | What it does | | --- | --- | --- | | loading | false | Show the skeleton instead of the children. | | children | — | The real content. Measured whenever it is on screen. | | sample | — | What to measure when the children cannot render yet. | | fallback | null | Shown until a measurement exists, and during SSR. | | id | component name | Cache identity. Set it if minification makes the name unstable. | | cache | globalLayoutCache | Pass your own LayoutCache to scope or clear measurements. | | breakpoints | 390 / 768 / 1280 | Measurements are keyed per breakpoint and retaken when one is crossed. | | premeasure | true | Measure in the background while the real content is visible. | | measure | "sample" | "sample" renders a stand-in off-screen; "live" walks the children in place. | | wrapper | — | Wraps the sample before measuring — to pin a locale or a theme that is not the live one. | | theme | — | baseColor, highlightColor, duration, animate, lineFill, fluid. | | walk | — | maxDepth, minSize, detectRepeats, collapseWrappers. | | onMeasure | — | Receives the layout map — useful for persisting it. |

Fixing a wrong guess

Classification is a heuristic: tags, computed styles and shape decide whether something is text, an image, a button or a circle. It is right most of the time and wrong sometimes, so the override lives next to the element:

<div data-skeleton-type="circle">JD</div>   <!-- initials, not a box -->
<div data-skeleton-ignore>debug panel</div> <!-- leave it out entirely -->

Theming

Options become CSS custom properties on the generated root, so several skeletons can be themed independently while sharing one stylesheet.

<AutoSkeleton
  loading={loading}
  theme={{ baseColor: "#1b2326", highlightColor: "#2f3c41", duration: 1.8 }}
>

The shimmer respects prefers-reduced-motion whatever you pass.

Also exported

  • SkeletonView — render a layout map you already have.
  • useBreakpoint() — the current breakpoint name; null until mounted.
  • measureReactElement(element, options) — measure an element in a detached root, for cases with no tree to portal out of. No context reaches it.
  • usePortalMeasurement() — the portal measurement itself, if you are building something other than AutoSkeleton on top of it.
  • The core types and cache utilities, so an app never needs a direct dependency on @iskeletor/core.

Good to know

  • The skeleton is inert: aria-hidden, no pointer events, no focusable nodes.
  • A component that renders nothing measurable is never cached, and its fallback stays up — better than pinning an empty skeleton in place for the session.
  • A sample that throws is caught by an error boundary around the measurement: the page keeps running, the fallback stays up, and the failure is a warning in development rather than an application crash.
  • A container that paints something - a card with a background, a border or a shadow - keeps that chrome in the skeleton, and only the content on it turns into bars. Without it a card layout reads as loose bars floating on the page. The shape is kept, the hue is not: each colour keeps its lightness and loses its hue, so a white card stays white and a tinted one turns grey rather than brand-coloured. theme: { surfaces: "original" } paints the measured colours for a surface that is the component - a bank card's gradient - and "none" leaves containers transparent.
  • Repeats fold in both shapes: a list folds along its stride, and a grid folds with the columns and row pitch as well. A layout that is neither - ragged rows, an off-centre last row - stays expanded, because a fold has to reproduce the measured positions exactly or not happen at all.

Licence

MIT