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-native-starlight-skeleton

v0.1.0

Published

Zero-layout-shift skeleton loader for React Native. Traverses your existing view tree with React.Children, reads each node's StyleSheet dimensions, and swaps leaves for shimmering Animated.View placeholders that match the final UI exactly.

Readme

react-native-starlight-skeleton

Zero-layout-shift skeleton loader for React Native. Wrap your real UI in <StarlightSkeleton loading> — it traverses the children tree, reads each node's StyleSheet dimensions, and swaps leaves for shimmering Animated.View placeholders that occupy exactly the boxes the final components will.

Features

  • Zero layout shift — placeholders are sized from your actual styles, so the transition from skeleton to content never reflows.
  • No skeleton markup to maintain — the loading state is derived from the same JSX as the loaded state; change the card, the skeleton follows.
  • One shared native animation — a single looping Animated.Value (useNativeDriver: true) drives every bone's opacity pulse in sync.
  • Zero runtime dependencies — only react and react-native peers.
  • Gradient-ready — inject any highlight component (e.g. react-native-linear-gradient) into every bone via one prop.
  • TypeScript-first, ESM + CJS builds, works on iOS and Android.

Install

npm install react-native-starlight-skeleton
# or
yarn add react-native-starlight-skeleton

Peer requirements: react 18 or 19, react-native >= 0.72.

Quickstart

import { useEffect, useState } from 'react';
import { Image, StyleSheet, Text, View } from 'react-native';
import { StarlightSkeleton } from 'react-native-starlight-skeleton';

export function ProfileCard() {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    fetchUser().then(setUser);
  }, []);

  return (
    <StarlightSkeleton loading={user === null}>
      <View style={styles.card}>
        <Image source={{ uri: user?.avatar }} style={styles.avatar} />
        <View style={styles.info}>
          <Text style={styles.name}>{user?.name}</Text>
          <Text style={styles.bio}>{user?.bio}</Text>
        </View>
      </View>
    </StarlightSkeleton>
  );
}

const styles = StyleSheet.create({
  card: { flexDirection: 'row', gap: 12, padding: 16 },
  avatar: { width: 48, height: 48, borderRadius: 24 },
  info: { flex: 1, gap: 6 },
  name: { fontSize: 16, lineHeight: 22, fontWeight: '600' },
  bio: { fontSize: 13, lineHeight: 18, color: '#666' },
});

While loading is true, the card renders as a row with a 48×48 circular bone and two text bars sized from your lineHeights. When it flips to false, your children render untouched — no wrapper, no cloned props.

A full item-card feed demo lives in example/FeedCardListScreen.tsx.

How it works

Traversal

When loading is true, the tree is compiled once per render with React.Children.map:

  • Leaves become bones. Text, Image, and TextInput elements (detected by component identity, with a displayName fallback), bare strings/numbers, and any element without children are replaced by a ShimmerBone sized from their style.
  • Containers are cloned. A View (or any element) with children is cloned via cloneElement, keeping its layout style, and its children are traversed recursively.
  • Depth is capped. Past maxDepth (default 10) a subtree collapses into a single bone.

Style extraction — why nothing shifts

Each node's style is run through StyleSheet.flatten and split:

  • Container styles (flexDirection, gap, padding*, alignItems, justifyContent, flexWrap) stay on the cloned container, so bones are laid out exactly where real children will sit. Containers also keep their own size/margin/flex box.
  • Size styles (width, height, min/max, aspectRatio, flex*, margin*, alignSelf, borderRadius) move onto the bone, so each placeholder occupies precisely the final component's box.
  • Text without an explicit height derives one: lineHeight if set, otherwise round(fontSize × 1.25) (React Native's default fontSize is 14, giving an 18pt bar). Text without an explicit width renders at '70%' so it reads as a line of text rather than a full-bleed block.
  • Images and explicitly sized nodes use their width/height directly.

Because the placeholder geometry is derived from the same StyleSheet as the real UI, swapping loading causes zero layout shift.

Animation

The root creates one Animated.Value and runs Animated.loop(Animated.sequence([timing 0→1, timing 1→0])) with useNativeDriver: true. Every bone interpolates that shared value into an opacity pulse (0.45 ↔ 1) over its solid baseColor — smooth and uniform on both iOS and Android, and off the JS thread. The loop stops and resets when loading turns off or the skeleton unmounts. The skeleton root is inert (pointerEvents="none") and announced to screen readers as accessibilityLabel="Loading".

Props

| Prop | Type | Default | Description | | --- | --- | --- | --- | | loading | boolean | — (required) | true renders the skeleton; false renders children untouched. | | children | ReactNode | — (required) | Your real UI. | | baseColor | string | '#E1E4EA' | Solid bone color. | | shimmerColor | string | '#F2F4F8' | Highlight color, forwarded to HighlightComponent. | | animationDuration | number | 1200 | Full pulse cycle in ms (0→1→0). | | borderRadius | number | 6 | Fallback bone radius when a node's style declares none. | | maxDepth | number | 10 | Max container recursion depth; deeper subtrees collapse to one bone. | | HighlightComponent | ComponentType<ShimmerHighlightProps> | — | Rendered inside every bone; receives progress, baseColor, shimmerColor. |

Also exported: ShimmerBone (aliased as Bone) for hand-rolled skeletons, StarlightContext, and all public types.

Gradient shimmer injection

The package ships dependency-free, but every bone will render an injected highlight — the classic sweeping-gradient look with react-native-linear-gradient:

import { Animated, StyleSheet } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {
  StarlightSkeleton,
  type ShimmerHighlightProps,
} from 'react-native-starlight-skeleton';

const AnimatedGradient = Animated.createAnimatedComponent(LinearGradient);

function GradientHighlight({ progress, shimmerColor }: ShimmerHighlightProps) {
  const translateX = progress
    ? progress.interpolate({ inputRange: [0, 1], outputRange: [-200, 200] })
    : 0;
  return (
    <AnimatedGradient
      colors={['transparent', shimmerColor, 'transparent']}
      start={{ x: 0, y: 0.5 }}
      end={{ x: 1, y: 0.5 }}
      style={[StyleSheet.absoluteFill, { transform: [{ translateX }] }]}
    />
  );
}

<StarlightSkeleton loading HighlightComponent={GradientHighlight}>
  {/* ... */}
</StarlightSkeleton>;

Limitations

  • Custom components are treated as containers. Traversal sees elements, not rendered output: a custom component with children is cloned and its children traversed; one without children becomes a single bone sized from its style prop. If a custom component renders its own internals, wrap the skeleton around its output styles or give it explicit dimensions.
  • Dynamic widths are approximated. Text measured at runtime (no explicit width/flex) falls back to a '70%' bar; content-driven Image sizes (no style dimensions) produce a zero-height bone — always give skeletonized images explicit dimensions or aspectRatio.
  • Percentages resolve against the skeleton's container, which is the cloned original container — identical box, so this is normally invisible.
  • Styles supplied through mechanisms other than the style prop (e.g. className-based systems that inject styles at render time) are not seen by extraction.

License

MIT © 2026 Dinesh Kumar