30 CSS Keyframe Animations 10 / 30

CSS Skeleton Loading Animation

Shimmer skeleton loading placeholders for blog cards, profile layouts, list rows and dashboard stat cards using a scoped linear-gradient sweep keyframe.

Pure CSS MIT licensed
Live Demo Open in tab

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

Open in playground

The code

<div class="kf-10">
  <div class="kf-10__title">Skeleton <span>Loading</span> Screens</div>
  <div class="kf-10__grid">

    <!-- Blog card skeletons -->
    <div class="kf-10__card">
      <div class="kf-10__skel kf-10__img-skel"></div>
      <div class="kf-10__skel kf-10__line"></div>
      <div class="kf-10__skel kf-10__line--sm"></div>
      <div class="kf-10__skel kf-10__line--xs"></div>
      <div class="kf-10__avatar-row">
        <div class="kf-10__skel kf-10__av-skel"></div>
        <div class="kf-10__skel kf-10__name-skel"></div>
      </div>
    </div>
    <div class="kf-10__card">
      <div class="kf-10__skel kf-10__img-skel"></div>
      <div class="kf-10__skel kf-10__line"></div>
      <div class="kf-10__skel kf-10__line--sm"></div>
      <div class="kf-10__skel kf-10__line--xs"></div>
      <div class="kf-10__avatar-row">
        <div class="kf-10__skel kf-10__av-skel"></div>
        <div class="kf-10__skel kf-10__name-skel"></div>
      </div>
    </div>

    <!-- Profile skeleton -->
    <div class="kf-10__profile">
      <div class="kf-10__skel kf-10__profile-av"></div>
      <div class="kf-10__skel kf-10__profile-name"></div>
      <div class="kf-10__skel kf-10__profile-sub"></div>
      <div class="kf-10__profile-bars">
        <div class="kf-10__skel" style="height:11px"></div>
        <div class="kf-10__skel" style="height:11px;width:85%"></div>
        <div class="kf-10__skel" style="height:11px;width:70%"></div>
        <div class="kf-10__skel" style="height:11px;width:90%"></div>
      </div>
    </div>

    <!-- List skeleton -->
    <div class="kf-10__list" style="grid-column:span 2">
      <div class="kf-10__list-item"><div class="kf-10__skel kf-10__li-icon"></div><div class="kf-10__li-body"><div class="kf-10__skel kf-10__li-t"></div><div class="kf-10__skel kf-10__li-s"></div></div></div>
      <div class="kf-10__list-item"><div class="kf-10__skel kf-10__li-icon"></div><div class="kf-10__li-body"><div class="kf-10__skel kf-10__li-t"></div><div class="kf-10__skel kf-10__li-s"></div></div></div>
      <div class="kf-10__list-item"><div class="kf-10__skel kf-10__li-icon"></div><div class="kf-10__li-body"><div class="kf-10__skel kf-10__li-t"></div><div class="kf-10__skel kf-10__li-s"></div></div></div>
    </div>

    <div class="kf-10__card">
      <div class="kf-10__skel kf-10__img-skel"></div>
      <div class="kf-10__skel kf-10__line"></div>
      <div class="kf-10__skel kf-10__line--sm"></div>
    </div>

    <!-- Dashboard -->
    <div class="kf-10__dash">
      <div class="kf-10__stat-card"><div class="kf-10__skel kf-10__stat-num"></div><div class="kf-10__skel kf-10__stat-label"></div><div class="kf-10__skel kf-10__stat-bar"></div></div>
      <div class="kf-10__stat-card"><div class="kf-10__skel kf-10__stat-num"></div><div class="kf-10__skel kf-10__stat-label"></div><div class="kf-10__skel kf-10__stat-bar"></div></div>
      <div class="kf-10__stat-card"><div class="kf-10__skel kf-10__stat-num"></div><div class="kf-10__skel kf-10__stat-label"></div><div class="kf-10__skel kf-10__stat-bar"></div></div>
      <div class="kf-10__stat-card"><div class="kf-10__skel kf-10__stat-num"></div><div class="kf-10__skel kf-10__stat-label"></div><div class="kf-10__skel kf-10__stat-bar"></div></div>
    </div>
  </div>
</div>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
.kf-10,.kf-10 *,.kf-10 *::before,.kf-10 *::after{box-sizing:border-box;margin:0;padding:0}
.kf-10 ::selection{background:#7c3aed;color:#fff}
.kf-10{
  --bg:#f5f3ff;
  --card:#fff;
  --base:#e5e7eb;
  --shine:rgba(255,255,255,.9);
  --purple:#7c3aed;
  --purple-light:#ede9fe;
  font-family:'Inter',sans-serif;
  background:var(--bg);
  min-height:100vh;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  padding:40px 20px;gap:40px;
  color:#1f2937;
}
.kf-10__title{font-size:1.6rem;font-weight:600;color:#1f2937;letter-spacing:-.02em}
.kf-10__title span{color:var(--purple)}
.kf-10__grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;max-width:900px;width:100%}

/* Shimmer core */
.kf-10__skel{
  background:var(--base);
  border-radius:6px;
  position:relative;
  overflow:hidden;
}
.kf-10__skel::after{
  content:'';
  position:absolute;inset:0;
  background:linear-gradient(90deg,transparent 0%,var(--shine) 50%,transparent 100%);
  animation:kf-10-shimmer 1.6s ease-in-out infinite;
}
@keyframes kf-10-shimmer{
  0%{transform:translateX(-100%)}
  100%{transform:translateX(100%)}
}

/* 1 — Blog card skeleton */
.kf-10__card{background:var(--card);border-radius:16px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.08);display:flex;flex-direction:column;gap:12px}
.kf-10__img-skel{height:140px;border-radius:10px}
.kf-10__line{height:14px}
.kf-10__line--sm{height:12px;width:70%}
.kf-10__line--xs{height:10px;width:50%}
.kf-10__avatar-row{display:flex;gap:10px;align-items:center;margin-top:4px}
.kf-10__av-skel{width:32px;height:32px;border-radius:50%;flex:0 0 32px}
.kf-10__name-skel{height:11px;flex:0 0 90px}

/* 2 — Profile skeleton */
.kf-10__profile{background:var(--card);border-radius:16px;padding:24px;box-shadow:0 1px 3px rgba(0,0,0,.08);display:flex;flex-direction:column;align-items:center;gap:14px}
.kf-10__profile-av{width:72px;height:72px;border-radius:50%}
.kf-10__profile-name{height:16px;width:120px}
.kf-10__profile-sub{height:12px;width:80px}
.kf-10__profile-bars{width:100%;display:flex;flex-direction:column;gap:8px}

/* 3 — List skeleton */
.kf-10__list{background:var(--card);border-radius:16px;padding:20px;box-shadow:0 1px 3px rgba(0,0,0,.08);display:flex;flex-direction:column;gap:0}
.kf-10__list-item{display:flex;gap:12px;align-items:center;padding:12px 0;border-bottom:1px solid var(--bg)}
.kf-10__list-item:last-child{border:0;padding-bottom:0}
.kf-10__li-icon{width:40px;height:40px;border-radius:10px;flex:0 0 40px}
.kf-10__li-body{flex:1;display:flex;flex-direction:column;gap:7px}
.kf-10__li-t{height:13px}
.kf-10__li-s{height:10px;width:65%}

/* 4 — Dashboard skeleton */
.kf-10__dash{grid-column:span 3;background:var(--card);border-radius:16px;padding:24px;box-shadow:0 1px 3px rgba(0,0,0,.08);display:grid;grid-template-columns:repeat(4,1fr);gap:20px}
.kf-10__stat-card{background:var(--purple-light);border-radius:12px;padding:18px;display:flex;flex-direction:column;gap:10px}
.kf-10__stat-num{height:28px;width:80px}
.kf-10__stat-label{height:11px;width:60%}
.kf-10__stat-bar{height:5px;border-radius:3px;margin-top:4px}

/* Stagger delays for variety */
.kf-10__card:nth-child(2) .kf-10__skel::after{animation-delay:.2s}
.kf-10__card:nth-child(3) .kf-10__skel::after{animation-delay:.4s}
.kf-10__list-item:nth-child(2) .kf-10__skel::after{animation-delay:.15s}
.kf-10__list-item:nth-child(3) .kf-10__skel::after{animation-delay:.3s}
.kf-10__stat-card:nth-child(2) .kf-10__skel::after{animation-delay:.1s}
.kf-10__stat-card:nth-child(3) .kf-10__skel::after{animation-delay:.2s}
.kf-10__stat-card:nth-child(4) .kf-10__skel::after{animation-delay:.3s}

@media(max-width:700px){.kf-10__grid{grid-template-columns:1fr}.kf-10__dash{grid-column:span 1;grid-template-columns:repeat(2,1fr)}}
@media(prefers-reduced-motion:reduce){.kf-10__skel::after{animation:none}}

How this works

Every skeleton element is a flat .kf-10__skel with a muted grey background and an absolutely-positioned ::after overlay containing a linear-gradient(90deg, transparent, var(--shine), transparent) highlight. The shimmer keyframe translates that pseudo-element from translateX(-100%) to translateX(100%) on a 1.6s ease-in-out loop, so the gleam sweeps across each block.

The layouts (blog card, profile, list rows, dashboard stats) reuse the same ::after mechanic. Variety comes from animation-delay on :nth-child selectors — the second card delays 0.2s, the third 0.4s, the dashboard stat cards each offset 0.1s, so the gleam looks like it cascades through the layout instead of striking everywhere at once.

Customize

  • Adjust the shimmer colour by editing the --shine custom property (default rgba(255,255,255,.9)) — drop to 0.5 alpha for subtler placeholders.
  • Slow the sweep by changing kf-10-shimmer duration from 1.6s to 2.4s for less frenetic loading.
  • Tweak base placeholder colour through --base: #e5e7eb for dark-mode skeletons.
  • Change the stagger by editing the :nth-child animation-delay values from .2s increments to .1s for tighter cascade.

Watch out for

  • The ::after shimmer requires its parent to have overflow: hidden — without it the gradient bleeds into siblings.
  • Don't show skeletons for under 300ms — the shimmer reads as broken rather than loading. Gate them behind a delay.
  • Animating transform on the ::after is cheap, but stacking 30+ skeleton blocks still costs composited layers — limit to what's visible above the fold.

Browser support

ChromeSafariFirefoxEdge
60+ 12+ 60+ 60+

Search CodeFronts

Loading…