Home / Snippets / UI Components /

Skeleton card

Loading placeholder cards with a CSS shimmer animation — no JavaScript needed to signal that content is on its way.

Widely Supported
uianimationno-js

Quick implementation

.skeleton {
  background: oklch(0.26 0.02 260);
  border-radius: 0.375rem;
  overflow: hidden;
  position: relative;
}

.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    transparent 0%,
    oklch(0.38 0.02 260 / 0.55) 50%,
    transparent 100%
  );
  background-size: 200% 100%;
  background-position: -200% 0;
  animation: shimmer 1.6s ease-in-out infinite;
}

@keyframes shimmer {
  to { background-position: 200% 0; }
}

/* Shape variants */
.skeleton--rect   { width: 100%; height: 8rem; }
.skeleton--title  { width: 70%; height: 1rem; }
.skeleton--line   { width: 100%; height: 0.75rem; }
.skeleton--btn    { width: 6rem; height: 2rem; border-radius: 1rem; }

@media (prefers-reduced-motion: reduce) {
  .skeleton::after {
    animation: none;
    background: oklch(0.32 0.02 260 / 0.5);
  }
}

Prompt this to your LLM

Includes role, goal, technical constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building loading states for a content UI.

Goal: A skeleton card component with a CSS shimmer animation that signals
content is loading — no JavaScript required.

Technical constraints:
- Each skeleton placeholder is a <div class="skeleton"> element sized with
  utility modifier classes (.skeleton--rect, .skeleton--title, .skeleton--line,
  .skeleton--btn).
- The shimmer effect is a ::after pseudo-element with a linear-gradient at 90deg,
  moving from background-position: -200% 0 to 200% 0. Use background-size: 200% 100%
  so the gradient is twice the element width and sweeps fully across.
- Animate with: animation: shimmer 1.6s ease-in-out infinite;
- Use oklch() for all colors — no hex or rgba values.
- Use CSS custom properties (var(--card), var(--radius), etc.) for theming.
- Include @media (prefers-reduced-motion: reduce) that removes the animation
  and instead applies a static semi-transparent overlay so the skeleton is
  still visually distinct without motion.

Framework variant (pick one):
A) Vanilla CSS — plain .skeleton class + modifier classes applied to divs.
B) React component — accept a <Skeleton> component with a variant prop
   ("rect" | "title" | "line" | "btn") and a width prop for overrides;
   output the correct element with className derived from variant.

Edge cases to handle:
- The parent card container should clip the shimmer overflow; ensure
  overflow: hidden on both .skeleton and any card wrapper.
- On very slow connections the shimmer should loop indefinitely; avoid
  animation-iteration-count: 1 or any auto-stop logic.
- If skeleton cards appear inside a CSS grid or flex layout, ensure the
  .skeleton--rect has a defined height (not just aspect-ratio) to avoid
  collapsing to zero height when the parent has no intrinsic size.

Return CSS only (or a React component if variant B is chosen).

Why skeleton screens beat spinners

Perceived performance is not the same as actual performance. Research — including Facebook's own study on skeleton UIs — consistently shows that users rate skeleton screens as faster than spinner-based loaders, even when the actual load time is identical. The reason is cognitive: a skeleton card gives the brain a structural preview of what is coming. The eye starts interpreting the layout before a single byte of real content arrives, so the transition from placeholder to live content feels instant rather than abrupt.

Spinners communicate exactly one bit of information: "something is happening." Skeleton screens communicate structure, count, and relative size of the incoming content. For card grids, article feeds, or social timelines, that extra information dramatically reduces the sense of waiting.

The gradient shimmer technique

The shimmer works by setting background-size: 200% 100% on a linear-gradient so the gradient is twice the width of its element. At background-position: -200% 0 the bright midpoint of the gradient sits entirely to the left, off-screen. Animating to background-position: 200% 0 sweeps the bright band fully across the element and off the right edge — a complete left-to-right pass.

The gradient itself uses three stops: transparent → light → transparent. The light stop is a muted, slightly brighter oklch() color rather than pure white, so the shimmer blends naturally with dark-mode card surfaces without looking jarring. Using oklch() lets you adjust lightness and chroma independently: raise the L value slightly for the midpoint stop, keep the same hue and chroma as the card background.

The pseudo-element approach — attaching the shimmer to ::after rather than the element itself — keeps the skeleton's background independent of the shimmer. The base background on .skeleton provides the solid placeholder color; ::after layers the animated gradient on top via position: absolute; inset: 0. This separation means you can adjust the base color and shimmer color separately without rewriting the animation.

Reduced-motion alternative

The prefers-reduced-motion: reduce media query catches users who have enabled the "reduce motion" system preference — common among people with vestibular disorders, migraine sensitivity, or epilepsy. For those users, the shimmer animation is replaced with a static semi-transparent overlay on ::after.

The static overlay still distinguishes the skeleton from the surrounding card background, so the UI remains comprehensible without any movement. A subtle pulse (animation: pulse 2s ease-in-out infinite toggling opacity between 0.4 and 0.7) is an acceptable middle ground if you want to retain some life in the placeholder while keeping movement minimal — opacity changes are gentler than positional shifts.

Whichever approach you choose, never skip the prefers-reduced-motion query on looping animations. A skeleton card can loop for many seconds on a slow connection, and continuous lateral motion during that time can be genuinely harmful for sensitive users.