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.
  • wave anchors 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
@soroush
Portrait of Masoud Soroush, Principal Software Engineer

Building 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>

Skeleton API reference