Skip to content

Skeleton

Skeleton provides a comprehensive loading placeholder solution with smooth animations and multiple variants. It includes basic skeleton shapes, multi-line text skeletons, and circular avatars to match your content structure while data is being fetched.

import { Skeleton, SkeletonText, SkeletonCircle } from 'rnc-theme';
<Skeleton width="100%" height={20} />
PropTypeDefaultDescription
widthDimensionValue'100%'Width of the skeleton element
heightDimensionValue20Height of the skeleton element
borderRadiuskeyof Theme['components']['borderRadius']'md'Border radius value
animatedbooleantrueEnable pulsing animation
sizeComponentSize'md'Size variant (xs, sm, md, lg, xl)
disabledbooleanfalseDisable animations and reduce opacity
styleStyleProp<ViewStyle>-Additional view styles
PropTypeDefaultDescription
linesnumber3Number of text lines to display
lineHeightnumber20Height of each text line
lastLineWidthDimensionValue'60%'Width of the last line (shorter for natural look)
animatedbooleantrueEnable pulsing animation
sizeComponentSize'md'Size variant affecting line height
disabledbooleanfalseDisable animations and reduce opacity
styleStyleProp<ViewStyle>-Additional container styles
PropTypeDefaultDescription
diameternumber40Diameter of the circular skeleton
animatedbooleantrueEnable pulsing animation
sizeComponentSize'md'Size variant (affects default diameter)
disabledbooleanfalseDisable animations and reduce opacity
styleStyleProp<ViewStyle>-Additional view styles
SizeHeight (Skeleton)Line Height (SkeletonText)Diameter (SkeletonCircle)
xs12px12px24px
sm16px16px32px
md20px20px40px
lg24px24px48px
xl32px32px64px
<VStack spacing="lg" padding="lg">
{/* Header Skeleton */}
<Skeleton width="70%" height={28} borderRadius="sm" />
{/* Paragraph Skeleton */}
<SkeletonText lines={4} lineHeight={16} lastLineWidth="45%" />
{/* Button Skeleton */}
<Skeleton width={120} height={40} borderRadius="md" />
</VStack>
<HStack spacing="md" padding="lg" align="center">
{/* Avatar */}
<SkeletonCircle diameter={60} />
<VStack spacing="sm" flex={1}>
{/* Name */}
<Skeleton width="60%" height={20} />
{/* Username */}
<Skeleton width="40%" height={16} size="sm" />
{/* Bio */}
<SkeletonText lines={2} lineHeight={14} lastLineWidth="80%" />
</VStack>
</HStack>
<VStack spacing="md" padding="md" borderRadius="lg" backgroundColor="surface">
{/* Product Image */}
<Skeleton width="100%" height={200} borderRadius="md" />
<VStack spacing="sm">
{/* Product Title */}
<Skeleton width="85%" height={18} />
{/* Price */}
<Skeleton width="30%" height={24} />
{/* Description */}
<SkeletonText lines={3} lineHeight={14} lastLineWidth="60%" />
{/* Action Buttons */}
<HStack spacing="sm">
<Skeleton width={100} height={36} borderRadius="md" />
<Skeleton width={36} height={36} borderRadius="md" />
</HStack>
</VStack>
</VStack>
<VStack spacing="md">
{Array.from({ length: 5 }).map((_, index) => (
<HStack key={index} spacing="md" padding="sm" align="center">
<SkeletonCircle diameter={40} />
<VStack spacing="xs" flex={1}>
<Skeleton width="70%" height={16} />
<Skeleton width="50%" height={12} size="sm" />
</VStack>
<Skeleton width={24} height={24} borderRadius="sm" />
</HStack>
))}
</VStack>
<ScrollView padding="lg">
<VStack spacing="lg">
{/* Article Header */}
<VStack spacing="md">
<Skeleton width="95%" height={32} borderRadius="sm" />
<Skeleton width="40%" height={16} size="sm" />
</VStack>
{/* Featured Image */}
<Skeleton width="100%" height={240} borderRadius="lg" />
{/* Article Content */}
<VStack spacing="md">
<SkeletonText lines={6} lineHeight={18} lastLineWidth="70%" />
<Skeleton width="100%" height={180} borderRadius="md" />
<SkeletonText lines={8} lineHeight={18} lastLineWidth="55%" />
</VStack>
{/* Author Info */}
<HStack spacing="sm" padding="md" align="center">
<SkeletonCircle diameter={32} />
<VStack spacing="xs" flex={1}>
<Skeleton width="40%" height={14} size="sm" />
<Skeleton width="30%" height={12} size="xs" />
</VStack>
</HStack>
</VStack>
</ScrollView>
<VStack spacing="lg" padding="lg">
{/* Stats Cards */}
<HStack spacing="md">
{Array.from({ length: 3 }).map((_, index) => (
<VStack
key={index}
spacing="sm"
padding="md"
flex={1}
borderRadius="lg"
backgroundColor="surface"
>
<Skeleton width="60%" height={16} size="sm" />
<Skeleton width="40%" height={28} />
<Skeleton width="80%" height={12} size="xs" />
</VStack>
))}
</HStack>
{/* Chart Placeholder */}
<VStack spacing="md" padding="md" borderRadius="lg" backgroundColor="surface">
<Skeleton width="50%" height={20} />
<Skeleton width="100%" height={300} borderRadius="md" />
</VStack>
{/* Recent Activity */}
<VStack spacing="md" padding="md" borderRadius="lg" backgroundColor="surface">
<Skeleton width="40%" height={20} />
{Array.from({ length: 4 }).map((_, index) => (
<HStack key={index} spacing="sm" align="center">
<SkeletonCircle diameter={24} />
<VStack spacing="xs" flex={1}>
<Skeleton width="80%" height={14} size="sm" />
<Skeleton width="50%" height={12} size="xs" />
</VStack>
<Skeleton width="60px" height={12} size="xs" />
</HStack>
))}
</VStack>
</VStack>
const CustomSkeletonCard = () => {
return (
<VStack spacing="md" padding="lg">
{/* Fast animation for quick content */}
<Skeleton
width="100%"
height={20}
animated={true}
// Animation controlled by the component internally
/>
{/* Static skeleton for already loaded sections */}
<SkeletonText
lines={3}
animated={false}
style={{ opacity: 0.3 }}
/>
</VStack>
);
};
const ConditionalSkeletonCard = ({ data, loading }) => {
if (loading) {
return (
<VStack spacing="md" padding="lg">
<HStack spacing="md" align="center">
<SkeletonCircle diameter={50} />
<VStack spacing="sm" flex={1}>
<Skeleton width="70%" height={18} />
<Skeleton width="50%" height={14} size="sm" />
</VStack>
</HStack>
<SkeletonText lines={4} lineHeight={16} />
<HStack spacing="sm">
<Skeleton width={80} height={32} borderRadius="md" />
<Skeleton width={100} height={32} borderRadius="md" />
</HStack>
</VStack>
);
}
return (
<VStack spacing="md" padding="lg">
<HStack spacing="md" align="center">
<Image source={{ uri: data.avatar }} style={{ width: 50, height: 50 }} />
<VStack spacing="xs" flex={1}>
<Text variant="h6">{data.name}</Text>
<Text variant="body2" color="textSecondary">{data.email}</Text>
</VStack>
</HStack>
<Text variant="body1">{data.description}</Text>
<HStack spacing="sm">
<Button variant="primary" size="sm">
<ButtonText>Follow</ButtonText>
</Button>
<Button variant="outline" size="sm">
<ButtonText>Message</ButtonText>
</Button>
</HStack>
</VStack>
);
};
const ProgressiveSkeletonList = ({ items, loading, error }) => {
if (error) {
return <ErrorMessage message="Failed to load content" />;
}
return (
<VStack spacing="md">
{items.map((item, index) => (
<HStack key={item.id || index} spacing="md" padding="sm" align="center">
{item.avatar ? (
<Image source={{ uri: item.avatar }} style={{ width: 40, height: 40 }} />
) : (
<SkeletonCircle diameter={40} />
)}
<VStack spacing="xs" flex={1}>
{item.title ? (
<Text variant="subtitle1">{item.title}</Text>
) : (
<Skeleton width="70%" height={16} />
)}
{item.subtitle ? (
<Text variant="body2" color="textSecondary">{item.subtitle}</Text>
) : (
<Skeleton width="50%" height={12} size="sm" />
)}
</VStack>
{item.action ? (
<Button variant="ghost" size="sm">
<ButtonText>{item.action}</ButtonText>
</Button>
) : (
<Skeleton width={60} height={28} borderRadius="md" />
)}
</HStack>
))}
{loading && (
<HStack spacing="md" padding="sm" align="center">
<SkeletonCircle diameter={40} />
<VStack spacing="xs" flex={1}>
<Skeleton width="70%" height={16} />
<Skeleton width="50%" height={12} size="sm" />
</VStack>
<Skeleton width={60} height={28} borderRadius="md" />
</HStack>
)}
</VStack>
);
};
const StaggeredSkeletons = () => {
return (
<VStack spacing="md">
{Array.from({ length: 5 }).map((_, index) => (
<Skeleton
key={index}
width="100%"
height={20}
animated={true}
style={{
animationDelay: `${index * 100}ms` // Staggered effect
}}
/>
))}
</VStack>
);
};

Content Structure

  • Match skeleton shapes to actual content dimensions
  • Use realistic proportions for text lines and images
  • Maintain consistent spacing between skeleton elements

Loading States

  • Show skeletons immediately when loading starts
  • Use progressive loading for better perceived performance
  • Replace skeletons with actual content smoothly

Performance

  • Use animated={false} for static placeholders
  • Limit the number of animated skeletons on screen
  • Consider disabling animations on low-end devices
const useUserProfile = (userId) => {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUserProfile(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
const UserProfileCard = ({ userId }) => {
const { data: user, isLoading, error } = useUserProfile(userId);
if (error) {
return <ErrorMessage message="Failed to load user profile" />;
}
if (isLoading) {
return (
<VStack spacing="md" padding="lg">
<HStack spacing="md" align="center">
<SkeletonCircle diameter={80} />
<VStack spacing="sm" flex={1}>
<Skeleton width="70%" height={24} />
<Skeleton width="50%" height={16} size="sm" />
<Skeleton width="60%" height={14} size="sm" />
</VStack>
</HStack>
<SkeletonText lines={3} lineHeight={16} lastLineWidth="40%" />
</VStack>
);
}
return (
<VStack spacing="md" padding="lg">
<HStack spacing="md" align="center">
<Image source={{ uri: user.avatar }} style={{ width: 80, height: 80 }} />
<VStack spacing="xs" flex={1}>
<Text variant="h5">{user.name}</Text>
<Text variant="body2" color="textSecondary">{user.email}</Text>
<Text variant="caption">{user.location}</Text>
</VStack>
</HStack>
<Text variant="body1">{user.bio}</Text>
</VStack>
);
};