Quick Start
One package, zero native code. The bundler picks the right build automatically — Metro for React Native, Webpack/Vite for web.
1. Install
2. Wrap your component
3. Pass two props
4. (Optional) Global theme
Animations
expo-linear-gradient or react-native-linear-gradient. Both are auto-detected, no config needed. On web, CSS gradients are used instead.How skelter works
Most skeleton libs ask you to describe the layout manually. skelter does the opposite: it renders your real component, measures it live, and generates bones from that. Here is exactly what happens under the hood.
You wrap your component once
withSkeleton is a Higher-Order Component. It injects three props into your component: hasSkeleton, isLoading, and skeletonConfig. Your component itself does not change at all.
Invisible warmup render
When isLoading is true, skelter renders your component completely hidden (visibility: hidden on web, opacity: 0 on native). This warmup gives the browser and native runtime a chance to lay out all elements with real dimensions before measuring anything.
null on first mount (common with async fetching), the component may render nothing, so there is nothing to measure. This is where mockProps comes in. See the Cold Start section below for details.Layout measurement
After the warmup render, skelter reads the actual layout of every element. The strategy differs by platform but the output is the same: a flat list of positioned bones.
Web
ResizeObserver fires on each element. getBoundingClientRect() gives position and size. getComputedStyle() reads the computed border-radius. Bones reflow automatically whenever the container resizes.
React Native
skelter reads _reactInternals / _reactFiber from the root native View, then walks the Fiber tree depth-first. Each leaf node is measured with stateNode.measure(). Falls back to root-only if fibers are inaccessible.
Bones generated, always in sync
Bones are absolutely positioned over the hidden component. When isLoading flips to false, the real component takes over. No transition needed. Because measurement is live, any layout change in your component is automatically reflected in the skeleton the next time it shows.
<SkeletonPlaceholder> that you fill manually. When your design changes, you update the real component and forget to update the skeleton. They drift apart. With skelter you write the component once and you are done forever.The blank screen problem, solved
Your data arrives asynchronously. On first mount, article is null. Your component renders nothing. skelter tries to measure an empty container and falls back to a single gray block. Not what you want.
WITHOUT MOCKPROPS
WITH MOCKPROPS
title of "Lorem ipsum" gives a bone roughly the same width as a real title. Images can be null as long as the element has an explicit height in its style.Simple components
Atomic components are the smallest UI elements. withSkeleton wraps anything — one element, one bone, always perfectly sized.
Avatar
A circular profile picture. borderRadius: 50% is read from the element's style automatically — the bone is always a perfect circle.
Image Block
A hero or cover image. The bone inherits exact dimensions and border radius from your img / Image element.
Text Lines
Multiple text elements in a stack. skelter walks the tree and creates one bone per text node — each bone has the real measured width of that line.
Badges / Tags
Small pill-shaped labels. Every badge becomes its own bone, width and border-radius preserved.
Composed components
Molecules combine multiple atoms into a meaningful unit. skelter walks the entire component tree — one bone per element, always in sync with your real layout.
Profile Card
Avatar + name + role + bio. The fiber walk generates 4 bones: the avatar circle, and one bone per text node — each with its real measured width.
Article Card
Cover image + metadata + title + excerpt. Each element is measured independently — resize the window and bones reflow with your component.
Comment
Small avatar + author name + timestamp + body text. A common pattern in feeds and discussions — works identically in web and React Native.
Notification Item
Icon + title + meta + unread dot. The small dot on the right becomes its own perfectly circular bone.
Repeated items
Map over placeholder data and pass isLoadingSkeleton per item. On React Native, skelter auto-detects FlatList context and switches to root-only mode for smooth scrolling.
Article List
Four article rows with thumbnail, category, title, and meta. Each row is independently wrapped — skeletons can stagger their reveal as data arrives.
User List
Five user rows with avatar, online indicator, name, role, and a follow button. The small online dot becomes its own circular bone.
Array.map()
The simplest pattern — map over a placeholder array of nulls and pass isLoadingSkeleton per item. Works everywhere: web, ScrollView, any custom container.
FlatList
Drop-in for React Native FlatList. Pass null items while loading — skelter auto-detects the VirtualizedList context and switches to root-only mode for smooth 60fps scrolling.
FlashList
Same API with @shopify/flash-list — the fastest list renderer for React Native. Provide estimatedItemSize as usual, skelter handles the rest.
Full-page layouts
Wrap entire screens with withSkeleton. Use mockProps so the Fiber walk always has a realistic layout to measure — even before your data arrives.
Profile Screen
Overlapping avatar + banner + bio + stats row + posts grid. One withSkeleton call, ~15 bones generated automatically — no maintenance.
Dashboard / Stats Screen
Header + 2x2 stat grid + activity feed. The mockProps option pre-populates realistic data shapes so the skeleton always has correct dimensions on cold start.
Patterns
skelter is not tied to any data-fetching library. It only cares about one boolean: is the data ready? Below are recipes for the most common patterns.
React Query integration
useQuery returns isLoading — pass it straight to the component. Add mockProps once so the skeleton looks right even before the first fetch completes.
WRAP ONCE
USE ANYWHERE
Lists with React Query
Array(n).fill(null) as placeholder items while isLoading is true. Each null item gets a skeleton. When the real data arrives, React reconciles by key and swaps them in. The transition is instant.SWR integration
SWR separates initial load (isLoading) from background refresh (isValidating). Pick whichever makes sense for your UX.
BASIC
STALE-WHILE-REVALIDATE
isLoading in SWR v2 is true only when there is no cached data yet.isValidating is true on every request, including re-fetches. Most UIs should use isLoadingto avoid re-showing the skeleton on every focus event.FlatList + infinite scroll
FlatList with TanStack Query. Skeleton rows show on the initial load, disappear when data arrives, and never block scroll performance.
maxBonesInList to cap the number of skeleton rows if needed.Tuning and edge cases
Avoid a flash with minDuration
If your API responds in under 100ms the skeleton might flash for just one frame.minDuration: 600 holds it for at least half a second.
Exclude heavy native views
Components like MapView or VideoPlayer have native backing views that either fail to measure or produce confusing bones. Pass their displayName to exclude.
API Reference
Config priority: skeletonConfig prop → SkeletonTheme → defaults.
Props injected by withSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
hasSkeleton | boolean | — | Activates skeleton mode on this component. |
isLoading | boolean | — | Shows skeleton when true, real component when false. |
isLoadingSkeleton | boolean | — | Shorthand — sets both hasSkeleton and isLoading to true. |
skeletonConfig | SkeletonConfig | — | Per-instance config. Highest priority in the chain. |
SkeletonConfig
| Prop | Type | Default | Description |
|---|---|---|---|
animation | 'pulse' | 'wave' | 'shiver' | 'shatter' | 'none' | 'pulse' | Animation style. |
color | string | '#E0E0E0' | Base bone color. |
highlightColor | string | '#F5F5F5' | Shimmer highlight color (wave / shiver). |
speed | 'slow' | 'normal' | 'rapid' | number | 'normal' | 0.5× / 1× / 2× or custom multiplier. |
borderRadius | number | 4 | Fallback radius when element has no explicit style. |
direction | 'ltr' | 'rtl' | 'ltr' | Shimmer direction for RTL layouts. |
minDuration | number | 0 | Minimum ms the skeleton stays visible. |
disabled | boolean | false | Never show skeleton when true. |
maxBonesInList | number | 0 | Max bones in FlatList (0 = unlimited). |
shatterConfig | ShatterConfig | — | Grid fragmentation options (see below). |
ShatterConfig
| Prop | Type | Default | Description |
|---|---|---|---|
gridSize | number | 6 | Number of columns in the grid. |
stagger | number | 80 | Delay in ms between each square. |
fadeStyle | 'random' | 'cascade' | 'radial' | 'random' | Square fade order. |
withSkeleton(Component, options?) — second argument
| Prop | Type | Default | Description |
|---|---|---|---|
measureStrategy | 'auto' | 'root-only' | 'auto' | 'auto' walks the Fiber tree — one bone per element. 'root-only' restores single-block behaviour. |
maxDepth | number | 8 | Max depth of the Fiber tree traversal. |
exclude | string[] | [] | Component displayNames to skip. e.g. ['MapView', 'VideoPlayer']. |
mockProps | Record<string, unknown> | {} | Props used for the invisible warmup render on cold start. |
Platform notes
React Native only: auto mode walks _reactInternals / _reactFiber — stable across React 17–18. Falls back to root-only if fibers are inaccessible.
Web only: uses ResizeObserver for continuous measurement — bones reflow automatically when the container is resized.
FlatList: components inside FlatList / FlashList auto-switch to root-only mode. shatter falls back to pulse silently for performance.