Skeleton
Displays a low-fidelity placeholder while content loads, reducing layout shift and signalling a loading state. Supports three shapes, two animations (or none), explicit or child-inferred dimensions, and derives all colors from theme tokens.
Animation sync
The two animations behave differently when several skeletons share a page:
pulse(default) fades opacity only, so it is size-independent - a small circle and a full-width bar fade identically and, when rendered together, stay in lockstep with no extra work.waveanchors its shimmer gradient to the viewport (background-attachment: fixed) rather than to each element. One light band sweeps across the whole page and every skeleton reveals the slice under it, so differing widths and heights all stay visually in sync.
Both animations are disabled automatically under prefers-reduced-motion: reduce.
Examples
Text
<Skeleton variant="text" animation="pulse" width={240} />
Variants
text
circular
rectangular
const { animation }: Partial<SkeletonProps> = { variant: 'text', animation: 'pulse' }
<Flex flexDirection="row" gap={4} alignItems="center">
{skeletonVariantTokens.map((v) => (
<Flex key={v} flexDirection="column" gap={1} alignItems="center">
<Skeleton
variant={v}
width={64}
height={v === 'text' ? undefined : 64}
animation={animation}
/>
<Typography variant="caption" color="secondary" m={0}>
{v}
</Typography>
</Flex>
))}
</Flex>
Animations
pulse
wave
false (none)
<Flex flexDirection="column" gap={3}>
{skeletonAnimationTokens.map((a) => (
<Flex key={String(a)} flexDirection="column" gap={1}>
<Typography variant="caption" color="secondary" m={0}>
{a === false ? 'false (none)' : a}
</Typography>
<Skeleton
variant="rectangular"
borderRadius="md"
width="100%"
height={24}
animation={a}
/>
</Flex>
))}
</Flex>
Wave In Sync
<Flex flexDirection="column" gap={2}>
<Skeleton variant="rectangular" borderRadius="md" width="90%" height={20} animation="wave" />
<Skeleton variant="rectangular" borderRadius="md" width="40%" height={20} animation="wave" />
<Skeleton variant="rectangular" borderRadius="md" width="65%" height={20} animation="wave" />
</Flex>
Inferred From Children
Loading title
const { animation }: Partial<SkeletonProps> = { variant: 'text', animation: 'pulse' }
<Skeleton animation={animation}>
<Typography variant="h3" m={0}>
Loading title
</Typography>
</Skeleton>
Media Card
const { animation }: Partial<SkeletonProps> = { variant: 'text', animation: 'pulse' }
<Flex flexDirection="row" gap={2} alignItems="center">
<Skeleton variant="circular" width={40} height={40} animation={animation} />
<Flex flexDirection="column" gap={1} flex={1}>
<Skeleton variant="text" width="60%" animation={animation} />
<Skeleton variant="text" width="40%" animation={animation} />
</Flex>
</Flex>
Post Card
Loading
Loaded
MS
Masoud Soroush
@soroushBuilding a design system from the ground up tokens, primitives, and the craft behind consistent, accessible components.
const { animation }: Partial<SkeletonProps> = { variant: 'text', animation: 'pulse' }
<Flex flexDirection="row" gap={4} alignItems="flex-start" flexWrap="wrap">
<Flex flexDirection="column" gap={1} width={360}>
<Typography variant="overline" color="secondary" m={0}>
Loading
</Typography>
<PostCardSkeleton animation={animation} />
</Flex>
<Flex flexDirection="column" gap={1} width={360}>
<Typography variant="overline" color="secondary" m={0}>
Loaded
</Typography>
<RealPostCard />
</Flex>
</Flex>
Post Card Grid
const { animation }: Partial<SkeletonProps> = { variant: 'text', animation: 'pulse' }
<Grid gridTemplateColumns="2fr 1fr" gap={3}>
{Array.from({ length: 4 }, (_, i) => (
<PostCardSkeleton key={i} animation={animation} />
))}
</Grid>