React Native Skeleton Loader
A skeleton loading placeholder for React Native with shimmer animation, configurable shapes (text lines, circles, rectangles), and a card preset.
Expo Snack
Code
import React, { useEffect, useRef } from "react";
import { Animated, StyleSheet, View, type DimensionValue, type ViewStyle } from "react-native";
/* ------------------------------------------------------------------ */
/* Pulse hook */
/* ------------------------------------------------------------------ */
function usePulse(): Animated.Value {
const anim = useRef(new Animated.Value(0.3)).current;
useEffect(() => {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(anim, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(anim, {
toValue: 0.3,
duration: 500,
useNativeDriver: true,
}),
])
);
loop.start();
return () => loop.stop();
}, [anim]);
return anim;
}
/* ------------------------------------------------------------------ */
/* Base skeleton shape */
/* ------------------------------------------------------------------ */
interface BaseProps {
style?: ViewStyle;
}
function SkeletonBase({ style }: BaseProps) {
const opacity = usePulse();
return <Animated.View style={[styles.base, style, { opacity }]} />;
}
/* ------------------------------------------------------------------ */
/* Skeleton.Line */
/* ------------------------------------------------------------------ */
interface LineProps {
width?: DimensionValue;
height?: number;
}
function Line({ width = "100%", height = 16 }: LineProps) {
return <SkeletonBase style={{ width, height, borderRadius: height / 2 }} />;
}
/* ------------------------------------------------------------------ */
/* Skeleton.Circle */
/* ------------------------------------------------------------------ */
interface CircleProps {
size?: number;
}
function Circle({ size = 48 }: CircleProps) {
return <SkeletonBase style={{ width: size, height: size, borderRadius: size / 2 }} />;
}
/* ------------------------------------------------------------------ */
/* Skeleton.Rect */
/* ------------------------------------------------------------------ */
interface RectProps {
width?: DimensionValue;
height?: number;
borderRadius?: number;
}
function Rect({ width = "100%", height = 100, borderRadius = 8 }: RectProps) {
return <SkeletonBase style={{ width, height, borderRadius }} />;
}
/* ------------------------------------------------------------------ */
/* Skeleton.Card */
/* ------------------------------------------------------------------ */
function Card() {
return (
<View style={styles.card}>
{/* Avatar row */}
<View style={styles.cardRow}>
<Circle size={44} />
<View style={styles.cardMeta}>
<Line width="50%" height={14} />
<Line width="30%" height={12} />
</View>
</View>
{/* Body lines */}
<View style={styles.cardBody}>
<Line width="100%" />
<Line width="92%" />
<Line width="60%" />
</View>
</View>
);
}
/* ------------------------------------------------------------------ */
/* Skeleton namespace */
/* ------------------------------------------------------------------ */
const Skeleton = {
Line,
Circle,
Rect,
Card,
};
/* ------------------------------------------------------------------ */
/* Demo App */
/* ------------------------------------------------------------------ */
export default function App() {
return (
<View style={styles.container}>
{/* Preset card */}
<Skeleton.Card />
<View style={styles.spacer} />
{/* Custom skeleton layout */}
<View style={styles.custom}>
<Skeleton.Rect width={80} height={80} borderRadius={12} />
<View style={styles.customLines}>
<Skeleton.Line width="60%" height={14} />
<Skeleton.Line width="90%" />
<Skeleton.Line width="40%" height={12} />
</View>
</View>
</View>
);
}
/* ------------------------------------------------------------------ */
/* Styles */
/* ------------------------------------------------------------------ */
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f172a",
paddingHorizontal: 20,
paddingTop: 64,
},
base: {
backgroundColor: "#374151",
},
card: {
backgroundColor: "#1e293b",
borderRadius: 16,
padding: 16,
},
cardRow: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
cardMeta: {
flex: 1,
gap: 6,
},
cardBody: {
marginTop: 16,
gap: 8,
},
spacer: {
height: 24,
},
custom: {
flexDirection: "row",
gap: 12,
backgroundColor: "#1e293b",
borderRadius: 16,
padding: 16,
},
customLines: {
flex: 1,
gap: 8,
justifyContent: "center",
},
});React Native Skeleton Loader
A lightweight skeleton placeholder system with a pulsing shimmer animation. Drop in pre-built shapes — lines, circles, and rectangles — or use the Skeleton.Card preset to instantly scaffold a social-media-style loading card.
Props
Skeleton.Line
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | string | 100% | Width of the line placeholder. |
height | number | 16 | Height in pixels. |
Skeleton.Circle
| Prop | Type | Default | Description |
|---|---|---|---|
size | number | 48 | Diameter of the circle. |
Skeleton.Rect
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | string | 100% | Width of the rect. |
height | number | 100 | Height in pixels. |
borderRadius | number | 8 | Corner radius. |
Skeleton.Card
No props — renders a ready-made social post placeholder (avatar circle, name line, and body lines).
Usage
{/* Ready-made card */}
<Skeleton.Card />
{/* Custom layout */}
<View style={{ flexDirection: "row", gap: 12 }}>
<Skeleton.Rect width={80} height={80} borderRadius={12} />
<View style={{ flex: 1, gap: 8 }}>
<Skeleton.Line width="60%" />
<Skeleton.Line width="90%" />
<Skeleton.Line width="40%" />
</View>
</View>
How it works
- A shared
usePulsehook creates a loopingAnimated.Valuethat oscillates between0.3and1over 1 second usingAnimated.loopwithAnimated.sequence. - Each shape component wraps its view in an
Animated.Viewwhoseopacityis bound to the pulse value, producing the shimmer effect. - All shapes use a dark gray background (
#374151) so they blend naturally on dark-themed screens.