UI Components Easy
Skeleton Loader
CSS-only shimmer skeleton loading placeholders for cards and content blocks. Zero JavaScript — pure CSS keyframe animation with a traveling shimmer highlight.
Open in Lab
MCP
css keyframes css-variables
Targets: HTML React Native
Expo Snack
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0f172a;
color: #f1f5f9;
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
}
.demo {
display: flex;
flex-direction: column;
gap: 1.25rem;
width: min(380px, 100%);
}
.demo-label {
font-size: 0.75rem;
color: #475569;
text-align: center;
letter-spacing: 0.04em;
}
/* ── Skeleton base ── */
.skeleton {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.04) 25%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0.04) 75%
);
background-size: 200% 100%;
animation: shimmer 1.6s ease-in-out infinite;
border-radius: 0.375rem;
}
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
background: rgba(255, 255, 255, 0.06);
}
}
@keyframes shimmer {
from {
background-position: 200% center;
}
to {
background-position: -200% center;
}
}
/* ── Skeleton shapes ── */
.skel-avatar {
width: 2.75rem;
height: 2.75rem;
border-radius: 50%;
flex-shrink: 0;
}
.skel-line {
height: 0.75rem;
border-radius: 0.375rem;
}
.skel-thumb {
width: 100%;
height: 9rem;
border-radius: 0.625rem;
}
/* ── Skeleton card layout ── */
.skel-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 1rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.skel-card__header {
display: flex;
align-items: center;
gap: 0.75rem;
}
.skel-card__meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.skel-card__body {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.skel-card__footer {
display: flex;
justify-content: space-between;
gap: 0.5rem;
padding-top: 0.25rem;
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Skeleton Loader</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<p class="demo-label">Loading state (always visible for demo)</p>
<!-- Profile skeleton card -->
<div class="skel-card">
<div class="skel-card__header">
<div class="skeleton skel-avatar"></div>
<div class="skel-card__meta">
<div class="skeleton skel-line" style="width: 55%"></div>
<div class="skeleton skel-line" style="width: 35%"></div>
</div>
</div>
<div class="skel-card__body">
<div class="skeleton skel-line" style="width: 100%"></div>
<div class="skeleton skel-line" style="width: 90%"></div>
<div class="skeleton skel-line" style="width: 75%"></div>
</div>
<div class="skeleton skel-thumb"></div>
<div class="skel-card__footer">
<div class="skeleton skel-line" style="width: 30%"></div>
<div class="skeleton skel-line" style="width: 20%"></div>
</div>
</div>
<!-- Second card -->
<div class="skel-card">
<div class="skel-card__header">
<div class="skeleton skel-avatar"></div>
<div class="skel-card__meta">
<div class="skeleton skel-line" style="width: 45%"></div>
<div class="skeleton skel-line" style="width: 30%"></div>
</div>
</div>
<div class="skel-card__body">
<div class="skeleton skel-line" style="width: 100%"></div>
<div class="skeleton skel-line" style="width: 80%"></div>
</div>
<div class="skeleton skel-thumb"></div>
<div class="skel-card__footer">
<div class="skeleton skel-line" style="width: 25%"></div>
<div class="skeleton skel-line" style="width: 18%"></div>
</div>
</div>
</div>
</body>
</html>import React, { useEffect, useRef } from "react";
import { View, Animated, StyleSheet, Text } from "react-native";
/* ── Skeleton primitives ──────────────────────────────── */
interface SkeletonBaseProps {
width?: number | string;
height?: number;
borderRadius?: number;
style?: object;
}
function SkeletonBase({ width = "100%", height = 16, borderRadius = 6, style }: SkeletonBaseProps) {
const opacity = useRef(new Animated.Value(0.3)).current;
useEffect(() => {
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 1,
duration: 800,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 800,
useNativeDriver: true,
}),
])
);
pulse.start();
return () => pulse.stop();
}, [opacity]);
return (
<Animated.View
style={[
{
width: width as any,
height,
borderRadius,
backgroundColor: "#334155",
opacity,
},
style,
]}
/>
);
}
/* ── Presets ───────────────────────────────────────────── */
function SkeletonLine({ width = "100%", height = 14, style }: SkeletonBaseProps) {
return <SkeletonBase width={width} height={height} borderRadius={4} style={style} />;
}
function SkeletonCircle({ size = 48, style }: { size?: number; style?: object }) {
return <SkeletonBase width={size} height={size} borderRadius={size / 2} style={style} />;
}
function SkeletonCard() {
return (
<View style={styles.card}>
<SkeletonBase width="100%" height={140} borderRadius={10} />
<View style={{ padding: 14, gap: 10 }}>
<SkeletonLine width="60%" height={16} />
<SkeletonLine width="100%" />
<SkeletonLine width="80%" />
</View>
</View>
);
}
const Skeleton = {
Line: SkeletonLine,
Circle: SkeletonCircle,
Card: SkeletonCard,
};
/* ── Demo ─────────────────────────────────────────────── */
export default function App() {
return (
<View style={demo.container}>
<Text style={demo.heading}>Skeleton Loader</Text>
{/* Card skeleton */}
<Text style={demo.sectionTitle}>Card skeleton</Text>
<Skeleton.Card />
{/* Custom layout */}
<Text style={[demo.sectionTitle, { marginTop: 28 }]}>Custom layout</Text>
<View style={demo.customRow}>
<Skeleton.Circle size={52} />
<View style={{ flex: 1, marginLeft: 14, gap: 8 }}>
<Skeleton.Line width="50%" height={16} />
<Skeleton.Line width="90%" />
<Skeleton.Line width="70%" />
</View>
</View>
<View style={[demo.customRow, { marginTop: 16 }]}>
<Skeleton.Circle size={52} />
<View style={{ flex: 1, marginLeft: 14, gap: 8 }}>
<Skeleton.Line width="40%" height={16} />
<Skeleton.Line width="100%" />
<Skeleton.Line width="60%" />
</View>
</View>
</View>
);
}
/* ── Styles ───────────────────────────────────────────── */
const styles = StyleSheet.create({
card: {
backgroundColor: "#1e293b",
borderRadius: 14,
overflow: "hidden",
width: "100%",
},
});
const demo = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f172a",
padding: 24,
justifyContent: "center",
},
heading: {
color: "#fff",
fontSize: 22,
fontWeight: "700",
textAlign: "center",
marginBottom: 28,
},
sectionTitle: {
color: "#94a3b8",
fontSize: 13,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 1,
marginBottom: 12,
},
customRow: {
flexDirection: "row",
alignItems: "center",
},
});Skeleton Loader
Pure CSS skeleton loading placeholders with a traveling shimmer effect. Use them as drop-in replacements while async content loads — no JavaScript required.
How it works
The shimmer is a linear-gradient that travels across each skeleton element using a @keyframes animation on background-position. All skeleton elements share the .skeleton utility class.
@keyframes shimmer {
from { background-position: -200% center; }
to { background-position: 200% center; }
}
Skeleton shapes included
- Text lines — varying widths to simulate natural text
- Avatar — circular skeleton for profile images
- Thumbnail — rectangular block for images
- Card layout — combined skeleton card matching a real content card
When to use it
- Content that loads asynchronously (API calls, images)
- Replace spinners for better perceived performance
- Any list or grid of cards