Skeleton Shimmer

Grey placeholder shapes hold the layout while a soft band of light sweeps across them, then the real content crossfades in.

  • CSS
  • Cards
  • Loop
  • Added

AI prompt

Build a React profile row with a CSS skeleton loading state. While loading, render a 40px circle and two bars (60% and 40% wide) in a light grey, each with a ::after band: a linear-gradient from transparent to white at 60% opacity and back, translated from -100% to 100% with a 1.4s linear infinite keyframe. Animate transform only. Set aria-busy="true" on the container while loading. When the data arrives, crossfade to the real content (avatar initials, name "Mei Lin", role "Motion designer") over 200ms. Under prefers-reduced-motion, stop the sweep and keep the static grey shapes. Add a toggle button to switch between the two states.

Source

Demo.tsx
'use client'

import { useState } from 'react'
import s from './demo.module.css'

export default function SkeletonShimmer({ labels }: { labels: Record<string, string> }) {
  const [loading, setLoading] = useState(true)

  return (
    <div className={s.wrap}>
      <div className={s.row} aria-busy={loading} aria-live="polite">
        {loading ? (
          <>
            <span className={`${s.bone} ${s.avatar}`} />
            <span className={s.lines}>
              <span className={s.bone} style={{ width: '60%' }} />
              <span className={s.bone} style={{ width: '40%' }} />
            </span>
          </>
        ) : (
          <div className={s.loaded}>
            <span className={s.avatar + ' ' + s.initials}>{Array.from(labels.name)[0]}</span>
            <span className={s.lines}>
              <strong className={s.name}>{labels.name}</strong>
              <span className={s.role}>{labels.role}</span>
            </span>
          </div>
        )}
      </div>
      <button type="button" className={s.toggle} onClick={() => setLoading(!loading)}>
        {loading ? labels.load : labels.reset}
      </button>
    </div>
  )
}
demo.module.css
.wrap {
  display: grid;
  justify-items: center;
  gap: 16px;
  width: min(280px, 100%);
}

.row {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 64px;
  padding: 12px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-md);
  background: var(--stage-surface);
}

.loaded {
  display: contents;
  animation: fade 200ms var(--ease-out);
}

.lines {
  display: grid;
  flex: 1;
  gap: 8px;
  min-width: 0;
}

.bone {
  position: relative;
  display: block;
  height: 10px;
  overflow: hidden;
  border-radius: 4px;
  background: var(--stage-rule);
}

.bone::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--stage-sheen), transparent);
  transform: translateX(-100%);
  animation: sweep 1.4s linear infinite;
}

.avatar {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
}

.initials {
  display: grid;
  place-items: center;
  background: var(--stage-accent);
  color: var(--stage-accent-ink);
  font: 600 var(--text-sm) / 1 var(--font-display);
}

.name { font: 600 var(--text-sm) / 1.2 var(--font-display); color: var(--stage-ink); }
.role { font-size: var(--text-xs); color: var(--stage-muted); }

.toggle {
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-sm);
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.toggle:hover { border-color: var(--stage-accent); }
.toggle:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

@keyframes sweep { to { transform: translateX(100%); } }
@keyframes fade { from { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .bone::after { animation: none; display: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.