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

react-zero-skeleton

v3.0.0

Published

The component IS the skeleton. Auto-measured at runtime, always in sync, zero maintenance. React & React Native.

Readme

react-zero-skeleton

npm version npm downloads bundle size TypeScript React Native React MIT License

→ Live demo · Docs


The skeleton loader is the first contact between your app and your user. That moment deserves care, not an afterthought maintained in a separate file that drifts from reality the moment you touch the real component.

react-zero-skeleton changes the relationship. The component IS its skeleton. Wrap it once. The library measures the real layout at runtime, generates one bone per element, and keeps everything in sync automatically. Forever.

// Before: two things to write, two things to maintain, two things to break.
function ArticleCard({ article }) { /* real component */ }
const ArticleCardSkeleton = () => ( /* a copy you'll forget to update */ )

// After: one thing.
const ArticleCard = withSkeleton(function ArticleCard({ article }) {
  return (
    <View>
      <Image source={{ uri: article.cover }} style={{ height: 160 }} />
      <Text style={s.title}>{article.title}</Text>
      <Text style={s.excerpt}>{article.excerpt}</Text>
    </View>
  )
})

<ArticleCard hasSkeleton isLoading={isLoading} article={data} />

Installation

npm install react-zero-skeleton

No native code. No pod install. No linking.

The bundler resolves the right build automatically:

  • React Native / Metro → native build (Fiber walk · onLayout · Animated)
  • React web / Next.js / Vite → web build (DOM · ResizeObserver · CSS animations)

How it works

On the first render, the component renders invisibly. react-zero-skeleton walks the React Fiber tree (on native) or the DOM (on web), measures every View / Image / Text, and captures position, size, and corner radius. The overlay - one bone per element, positioned exactly - replaces the hidden content until isLoading becomes false.

Layout changes propagate automatically. If you add a field to the card, the skeleton gains a bone. No manual sync required.


Quick start

Wrap once

import { withSkeleton } from 'react-zero-skeleton'

function ProfileCard({ user }) {
  return (
    <View style={s.wrap}>
      <Image source={{ uri: user.avatar }} style={s.avatar} />
      <Text style={s.name}>{user.name}</Text>
      <Text style={s.bio}>{user.bio}</Text>
    </View>
  )
}

export default withSkeleton(ProfileCard)

Use it

// Two props. That's it.
<ProfileCard hasSkeleton isLoading={isLoading} user={data} />

// Shorthand: activates hasSkeleton + isLoading at once
<ProfileCard isLoadingSkeleton user={data} />

Global theme

import { SkeletonTheme } from 'react-zero-skeleton'

<SkeletonTheme animation="wave" color="#E0E0E0" highlightColor="#F5F5F5">
  <App />
</SkeletonTheme>

Config priority: skeletonConfig prop > SkeletonTheme > defaults.


Animations

Nine animations. Each has a reason to exist.

| Animation | Character | |-----------|-----------| | pulse | Soft opacity fade. The default. Works everywhere, no dependencies. | | wave | Shimmer left to right. Classic and readable. | | shiver | Wider, more energetic wave. Good for large image placeholders. | | drip | Vertical shimmer sweeping top to bottom, each bone phase-shifted. | | slide | Bones drift upward while fading in. A gentle breathing effect. | | beat | Double heartbeat: scale + opacity. Ideal for health and real-time data UIs. | | shaker | A rapid horizontal tremor burst, then silence. For alert-style states. | | shatter | Each bone fragments into a grid of squares. The signature animation. | | none | Static placeholder. Useful with prefers-reduced-motion. |

// Global
<SkeletonTheme animation="shatter">…</SkeletonTheme>

// Per component
<ProfileCard hasSkeleton isLoading={isLoading} skeletonConfig={{ animation: 'beat' }} />

// Speed
skeletonConfig={{ animation: 'wave', speed: 'slow' }}   // 0.5×
skeletonConfig={{ animation: 'wave', speed: 'rapid' }}  // 2×
skeletonConfig={{ animation: 'wave', speed: 1.5 }}      // custom multiplier

wave and shiver require a LinearGradient peer on React Native: npx expo install expo-linear-gradient or npm install react-native-linear-gradient. On web, CSS gradients are used: no peer needed.

Cascade

When cascade > 0, each bone's animation starts delayed by bone.y × cascade ms, creating a top-to-bottom sequential wave through the component.

<SkeletonTheme animation="pulse" cascade={3}>
  {/* bone at y=0 starts immediately, bone at y=100 starts 300ms later */}
</SkeletonTheme>

Shatter

<ProfileCard
  hasSkeleton
  isLoading={isLoading}
  skeletonConfig={{
    animation: 'shatter',
    shatterConfig: {
      cellSize: 24,        // px - overrides column count
      stagger: 80,         // ms delay between squares
      fadeStyle: 'radial'  // 'random' | 'cascade' | 'radial'
    }
  }}
/>

Loading experience

The transition into and out of the skeleton is part of the experience. react-zero-skeleton gives you control over both.

Enter animation

Played when the skeleton first appears.

<SkeletonTheme enter="fadeUp">…</SkeletonTheme>
// 'fade' | 'fadeUp' | 'fadeDown' | 'fadeLeft' | 'fadeRight' | 'none'

Exit animation

Played when the skeleton disappears and content takes over.

<SkeletonTheme animation="shatter" exit="fadeUp" revealOnExit>…</SkeletonTheme>

revealOnExit keeps the real content visible underneath while the skeleton fades out, creating a reveal effect instead of an abrupt swap.


SkeletonBox

By default, container divs are skipped: only their text and image children receive bones. When a container is itself a visually meaningful shape (a stat card, a chip, a badge), wrap it with SkeletonBox to emit the box as a semi-transparent bone with its children rendered on top.

import { SkeletonBox } from 'react-zero-skeleton'

function Stat({ label, value }) {
  return (
    <SkeletonBox style={{ flex: 1, backgroundColor: '#eee', borderRadius: 10, padding: 12 }}>
      <Text style={{ width: 'fit-content' }}>{label}</Text>
      <Text style={{ fontWeight: '700', width: 'fit-content' }}>{value}</Text>
    </SkeletonBox>
  )
}

The box bone is always static (no animation). Children animate normally.


SkeletonIgnore

Wraps elements that should never receive a skeleton bone and always remain visible during loading: section headers, timestamps, decorative labels.

import { SkeletonIgnore } from 'react-zero-skeleton'

function PriceCard({ data }) {
  return (
    <View style={s.wrap}>
      <SkeletonIgnore>
        <Text style={s.label}>Live price</Text>
      </SkeletonIgnore>
      <Text style={s.value}>{data.price}</Text>
      <Text style={s.change}>{data.change}</Text>
    </View>
  )
}

The measurement layer skips SkeletonIgnore entirely. The text stays visible while the dynamic content animates around it.


SkeletonParagraph

A block of text measures as one tall rectangle, so its skeleton is a single solid pavé. Wrap it with SkeletonParagraph and it skeletons into several lines instead: body lines get a naturally ragged width and the last line is shortened, like real text.

import { SkeletonParagraph } from 'react-zero-skeleton'

function Article({ article }) {
  return (
    <View>
      <Text style={s.title}>{article.title}</Text>
      <SkeletonParagraph size="md">
        <Text style={s.body}>{article.body}</Text>
      </SkeletonParagraph>
    </View>
  )
}

Line count

size is a preset; lines overrides it with an exact count.

<SkeletonParagraph size="sm">…</SkeletonParagraph>   {/* 2 lines */}
<SkeletonParagraph size="md">…</SkeletonParagraph>   {/* 3 lines (default) */}
<SkeletonParagraph size="lg">…</SkeletonParagraph>   {/* 5 lines */}
<SkeletonParagraph lines={7}>…</SkeletonParagraph>  {/* exactly 7 */}

Alignment

The shortened last line follows the text alignment. Pass align explicitly, or leave it out and it is inherited from the wrapped component's textAlign (computed style on web, the wrapper's style on React Native).

{/* explicit */}
<SkeletonParagraph size="md" align="center">…</SkeletonParagraph>

{/* inherited: lines follow text-align: right */}
<div style={{ textAlign: 'right' }}>
  <SkeletonParagraph size="md"><p>{body}</p></SkeletonParagraph>
</div>

On React Native, textAlign lives on Text, not View. To inherit alignment, set textAlign on the SkeletonParagraph style, or pass the align prop (recommended for cross-platform).

Word mode

mode="words" breaks each line into word-sized bones separated by gaps, for a more text-like placeholder.

<SkeletonParagraph size="md" mode="words">
  <Text>{article.body}</Text>
</SkeletonParagraph>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | size | 'sm' \| 'md' \| 'lg' | 'md' | Line count preset (2 / 3 / 5) | | lines | number | — | Exact line count; overrides size | | align | 'left' \| 'center' \| 'right' | inherited → 'left' | Alignment of the lines | | mode | 'lines' \| 'words' | 'lines' | One bar per line, or word-sized bones | | style | ViewStyle / CSSProperties | — | Style for the wrapper |

All widths are deterministic, so they never flicker between renders. Works on web and React Native (iOS / Android).


Adaptive animation

Skelter detects nothing itself. You bring your own signal source (NetInfo, navigator.connection, expo-battery, Low Power Mode…) and feed the values. Skelter only maps them to an animation.

How it works

Pass conditions (your live signals) and an adaptive policy to SkeletonTheme or skeletonConfig. If a rule matches, its animation is used; if nothing matches, the animation prop you already set is used unchanged — the "everything is fine" case is free.

// React Native example (NetInfo + expo-battery)
import { useNetInfo } from '@react-native-community/netinfo';
import { useBatteryLevel, useLowPowerMode } from 'expo-battery';

function App() {
  const { type } = useNetInfo();
  const battery  = useBatteryLevel();   // 0..1
  const lowPower = useLowPowerMode();

  return (
    <SkeletonTheme
      animation="shatter"               // default: used when no rule matches
      conditions={{ network: type, battery, saveData: lowPower }}
      adaptive={[
        { when: { network: ['slow-2g', '2g'] }, use: 'none'  },
        { when: { saveData: true },             use: 'pulse' },
        { when: { batteryBelow: 0.2 },          use: 'pulse' },
        { when: { network: '3g' },              use: 'wave'  },
      ]}
    >
      <HomeScreen />
    </SkeletonTheme>
  );
}
// Web example (navigator.connection)
const conn = (navigator as any).connection;

<SkeletonTheme
  animation="wave"
  conditions={{
    network: conn?.effectiveType,
    saveData: conn?.saveData,
  }}
  adaptive={[
    { when: { network: ['slow-2g', '2g'] }, use: 'none'  },
    { when: { saveData: true },             use: 'pulse' },
  ]}
>

Rule matrix (declarative)

Rules are evaluated in order — the first match wins. Inside a rule, every key must hold (AND). Across rules, the first match is used (OR).

adaptive={[
  // AND: all conditions in a rule must hold
  { when: { network: '3g', saveData: true }, use: 'none' },

  // array = membership check
  { when: { network: ['slow-2g', '2g'] }, use: 'none' },

  // numeric threshold on battery (only matches when battery is known)
  { when: { batteryBelow: 0.2 }, use: 'pulse' },

  // custom signal — any key you put in conditions
  { when: { thermal: ['serious', 'critical'] }, use: 'none' },
]}

Function (escape hatch)

Return undefined to fall through to the base animation.

adaptive={(conditions) => {
  if (conditions.battery != null && conditions.battery < 0.15) return 'none';
  if (conditions.network === '3g' && conditions.saveData) return 'pulse';
  return undefined; // fall through → animation prop
}}

Per-element override

Like any SkeletonConfig, the per-element skeletonConfig takes priority over SkeletonTheme. Conditions deep-merge; the adaptive policy uses the most specific level.

// Global: wave on 3g
<SkeletonTheme animation="wave" conditions={...} adaptive={...}>

  // This card keeps its own policy regardless of the theme
  <HeavyCard
    hasSkeleton isLoading={loading}
    skeletonConfig={{
      animation: 'shatter',
      adaptive: [{ when: { batteryBelow: 0.3 }, use: 'pulse' }],
    }}
  />
</SkeletonTheme>

conditions fields

| Field | Type | Description | |-------|------|-------------| | network | NetworkType | 'offline' \| 'slow-2g' \| '2g' \| '3g' \| '4g' \| '5g' \| 'wifi' \| 'unknown' | | battery | number | Battery level 0..1 | | charging | boolean | Whether the device is charging | | saveData | boolean | OS data-saver / reduced-data preference | | deviceTier | 'low' \| 'mid' \| 'high' | Coarse device capability | | reducedMotion | boolean | Accessibility reduce-motion (handled automatically if omitted) | | [custom] | unknown | Any extra signal — matched by rule keys |


API Reference

Props added by withSkeleton

| Prop | Type | Description | |------|------|-------------| | hasSkeleton | boolean | Activates skeleton mode on this instance | | isLoading | boolean | Shows the skeleton when true | | isLoadingSkeleton | boolean | Shorthand: activates hasSkeleton + isLoading | | skeletonConfig | SkeletonConfig | Per-instance config override |

SkeletonConfig

| Prop | Type | Default | Description | |------|------|---------|-------------| | animation | 'pulse' \| 'wave' \| 'shiver' \| 'drip' \| 'slide' \| 'beat' \| 'shaker' \| 'shatter' \| 'none' | 'pulse' | Animation style | | color | string | '#E0E0E0' | Base bone color | | highlightColor | string | '#F5F5F5' | Highlight for wave / shiver / drip | | speed | 'slow' \| 'normal' \| 'rapid' \| number | 'normal' | Animation speed multiplier | | borderRadius | number | 4 | Fallback corner radius | | direction | 'ltr' \| 'rtl' | 'ltr' | Shimmer direction | | cascade | number | 0 | Stagger ms per pixel of vertical position | | enter | 'fade' \| 'fadeUp' \| 'fadeDown' \| 'fadeLeft' \| 'fadeRight' \| 'none' | 'none' | Skeleton enter animation | | exit | 'fade' \| 'fadeUp' \| 'fadeDown' \| 'fadeLeft' \| 'fadeRight' \| 'none' | 'fade' | Skeleton exit animation | | revealOnExit | boolean | false | Show real content under skeleton during exit | | minDuration | number | 0 | Minimum ms the skeleton stays visible | | disabled | boolean | false | Disables skeleton entirely | | shatterConfig | ShatterConfig | - | Shatter animation config | | maxBonesInList | number | 0 | Max bones in FlatList (0 = unlimited) |

withSkeleton(Component, options?)

| Option | Type | Default | Description | |--------|------|---------|-------------| | measureStrategy | 'auto' \| 'root-only' | 'auto' | auto = one bone per element; root-only = single block | | maxDepth | number | 8 | Max Fiber tree depth | | exclude | string[] | [] | Component displayNames to skip | | mockProps | object | {} | Props for the invisible warmup render |

mockProps: cold start

When real props carry no data on first load, mockProps provides a realistic layout for the warmup render:

withSkeleton(ArticleCard, {
  mockProps: { article: { title: 'Lorem ipsum', image: null } }
})

ShatterConfig

| Prop | Type | Default | Description | |------|------|---------|-------------| | cellSize | number | 0 (auto) | Fixed cell size in px - overrides column count | | stagger | number | 80 | ms delay between squares | | fadeStyle | 'random' \| 'cascade' \| 'radial' | 'random' | Square fade order |

registerSkeletonLeaf (React Native)

Registers custom image components as leaf elements in the Fiber walk:

import { registerSkeletonLeaf } from 'react-zero-skeleton'
registerSkeletonLeaf('FastImage', 'ExpoImage')

SkeletonTheme auto mode (React Native)

Injects hasSkeleton on all children via cloneElement. Then anywhere in the tree, only isLoading is needed:

<SkeletonTheme auto animation="wave" exclude={['MapView']}>
  <App />
</SkeletonTheme>

// Anywhere in the tree:
<ArticleCard isLoading={isLoading} />

FlatList

const items = isLoading ? Array(6).fill(null) : realData

<FlatList
  data={items}
  renderItem={({ item }) => (
    <ArticleCard
      article={item}
      hasSkeleton
      isLoading={item === null}
    />
  )}
/>

shatter falls back to pulse automatically inside FlatList / FlashList for performance. maxBonesInList caps the number of bones per cell when lists are long.


Accurate bones: fit-content

Block-level elements expand to fill their container by default, so their bone fills the full width even if the text is short. Add width: 'fit-content' (web) or alignSelf: 'flex-start' (React Native) to make each bone match the actual content width.

// Web
<p style={{ fontSize: 16, width: 'fit-content' }}>{title}</p>

// React Native
<Text style={{ fontSize: 16, alignSelf: 'flex-start' }}>{title}</Text>

Comparison

| Feature | react-zero-skeleton | react-native-auto-skeleton | react-content-loader | react-loading-skeleton | |---------|:-:|:-:|:-:|:-:| | Zero config | ✅ | ✅ | ❌ | ❌ | | Auto-generated from live layout | ✅ | ✅ | ❌ | ❌ | | React web support | ✅ | ❌ | ✅ | ✅ | | Enter / exit transitions | ✅ | ❌ | ❌ | ❌ | | Cascade stagger | ✅ | ❌ | ❌ | ❌ | | Shatter animation | ✅ | ❌ | ❌ | ❌ | | SkeletonBox / SkeletonIgnore | ✅ | ❌ | ❌ | ❌ | | No native code | ✅ | ❌ | ✅ | ✅ | | RTL support | ✅ | ❌ | ❌ | ✅ |


Limitations

Fiber walk reads React internals: per-element measurement accesses _reactInternals / _reactFiber, which are undocumented but stable across React 17-18-19. If the walk fails, react-zero-skeleton falls back to a single root bone silently.

wave / shiver require a gradient peer on React Native: without expo-linear-gradient or react-native-linear-gradient, they fall back to a solid placeholder.

Animations run on the JS thread: all React Native animations use the Animated API. Reanimated worklets are on the roadmap for long lists on low-end devices.


Contributing

npm install
npm run build
npm test

Open a PR against main with a changeset:

npx changeset

License

MIT © J-Ben