FSD Frontend System Design

Part V β€” The Craft Β· Lesson 10.3

Shimmer UI

In one lineShow the shape of the content while it loads β€” perceived speed improves even though nothing got faster.

Where you've seen itThe grey card outlines on a video grid, filling in one by one.

  • Time13 min
  • DiagramSpinner vs skeleton timeline + layout stability
  • Chapter10 Β· Low Level Design

Diagram

diagram
SAME 1.8s WAIT, DIFFERENT EXPERIENCE

  SPINNER
  0ms ─────────────── 1800ms ──────────►
  [      ⟳ spinning      ][ content ]
   "is this broken?"       sudden jump, layout shifts

  SKELETON
  0ms ─────────────── 1800ms ──────────►
  [ β–€β–€β–€ shaped placeholders ][ content ]
   "it's coming, and it'll     content lands in the SAME boxes
    look like that"            β†’ no layout shift (CLS = 0)
diagram
THE SKELETON MIRRORS THE REAL LAYOUT

  loading                       loaded
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€ β”‚    β”‚ [ thumbnail image    ] β”‚
  β”‚                        β”‚    β”‚                        β”‚
  β”‚ β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€β–€         β”‚    β”‚ How the Web Works      β”‚
  β”‚ β–€β–€β–€β–€β–€β–€β–€                β”‚    β”‚ 42K views Β· 2 days ago β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    same dimensions, same positions  ← this is the whole point
diagram
WHICH LOADER, WHEN

  < 300ms      nothing            a flash of loader is worse than none
  300ms–1s     spinner            too short for a skeleton to help
  > 1s, known  SKELETON           layout is predictable
    layout
  > 1s, unknown  progress bar     uploads, exports, long jobs
    duration
  background     inline indicator never block the whole screen

How it works

  1. Mirror the real layout exactly β€” same dimensions, spacing, and count. A skeleton that doesn't match reintroduces the layout shift it was meant to prevent.
  2. Delay by ~200–300ms so fast responses never flash a loader.
  3. Animate subtly β€” a slow shimmer sweep signals activity; anything fast or high-contrast is distracting.
  4. Respect prefers-reduced-motion and drop the animation to a static tint.
  5. Hide it from screen readers (aria-hidden) and announce the state change via a live region instead (8.3).
css
.skeleton {
  background: linear-gradient(90deg, var(--s2) 25%, var(--s3) 50%, var(--s2) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; background: var(--s2); }
}

Trade-offs

SkeletonSpinner
Sets expectations, no layout shiftTrivial to add anywhere
Must be maintained alongside layoutSays nothing about what's coming
Best for predictable contentFine for short or unknown waits

Better than both, where possible: render cached data immediately and revalidate behind it (6.9). The fastest loading state is the one the user never sees.

Interview angle

"Spinner or skeleton?"

  • Skeleton when the layout is known and the wait exceeds a second β€” it removes the layout shift and sets expectations.
  • Spinner for short or unpredictable waits; nothing at all under ~300ms.
  • Best of all is stale-while-revalidate, so returning users see content instantly and it refreshes underneath.

Recap

  • Skeletons buy perceived speed and protect CLS by reserving exact space.
  • Delay the loader ~300ms; never flash it.
  • Reduced motion and screen-reader handling are part of the component, not extras.