Accordion expandWeb · App
Maya ChenPushed 3 commits
Ravi PatelOpened a pull request
Lena FischerCommented on #42
Target
Implement the "Skeleton shimmer" effect in vanilla HTML/CSS/JS. Context: Placeholder blocks with a soft highlight sweeping across while content loads, then a crossfade to the real rows. Trigger: the page loads. Timeline: time-based (duration + easing). Motion tokens (normal): - translateX (highlight): -100% → 100% - opacity (skeleton → content): 1 → 0 → 0 → 1 - duration: 1400ms per sweep, infinite · 250ms crossfade - easing: linear (sweep) · ease-out (crossfade) - note: highlight is a gradient layer moved by transform, not background-position Motion tokens (prefers-reduced-motion): - opacity (skeleton → content): 1 → 0 → 0 → 1 - duration: 250ms - easing: ease-out - note: no sweep; static grey blocks Technique: prefer pure CSS transitions/animations; use JS only to toggle classes. Acceptance: - animate only transform and opacity unless the spec lists other properties - no layout shift; content is visible without the animation - when prefers-reduced-motion is set, use the reduced tokens only - skeleton has the same layout footprint as the loaded content so nothing jumps - sweep stops (animation removed) once content is shown
Generated from the spec. Prompts differ by target name only; the tokens are the source of truth.
demo.html
<div class="fx-skeleton-shimmer" data-fx-root>
<div class="fx-box">
<div class="fx-layer fx-skel" aria-hidden="true">
<div class="fx-row"><span class="fx-av"></span><span class="fx-lines"><span class="fx-l fx-w60"></span><span class="fx-l fx-w90"></span></span></div>
<div class="fx-row"><span class="fx-av"></span><span class="fx-lines"><span class="fx-l fx-w50"></span><span class="fx-l fx-w80"></span></span></div>
<div class="fx-row"><span class="fx-av"></span><span class="fx-lines"><span class="fx-l fx-w70"></span><span class="fx-l fx-w60"></span></span></div>
</div>
<div class="fx-layer fx-content">
<div class="fx-row"><span class="fx-av fx-c1"></span><span class="fx-lines"><strong>Maya Chen</strong><span>Pushed 3 commits</span></span></div>
<div class="fx-row"><span class="fx-av fx-c2"></span><span class="fx-lines"><strong>Ravi Patel</strong><span>Opened a pull request</span></span></div>
<div class="fx-row"><span class="fx-av fx-c3"></span><span class="fx-lines"><strong>Lena Fischer</strong><span>Commented on #42</span></span></div>
</div>
</div>
</div>
demo.css
.fx-skeleton-shimmer { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.fx-skeleton-shimmer .fx-box { position: relative; width: 70%; max-width: 340px; display: grid; }
.fx-skeleton-shimmer .fx-layer { grid-area: 1 / 1; display: flex; flex-direction: column; gap: 14px; transition: opacity 250ms ease-out; }
.fx-skeleton-shimmer .fx-row { display: flex; align-items: center; gap: 12px; }
.fx-skeleton-shimmer .fx-av { width: 34px; height: 34px; border-radius: 50%; flex: none; }
.fx-skeleton-shimmer .fx-lines { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; }
.fx-skeleton-shimmer .fx-skel .fx-av, .fx-skeleton-shimmer .fx-l { position: relative; overflow: hidden; background: var(--fx-muted, #e4e4e7); border-radius: 4px; }
.fx-skeleton-shimmer .fx-l { display: block; height: 9px; }
.fx-skeleton-shimmer .fx-w50 { width: 50%; } .fx-skeleton-shimmer .fx-w60 { width: 60%; } .fx-skeleton-shimmer .fx-w70 { width: 70%; } .fx-skeleton-shimmer .fx-w80 { width: 80%; } .fx-skeleton-shimmer .fx-w90 { width: 90%; }
.fx-skeleton-shimmer .fx-skel .fx-av::after, .fx-skeleton-shimmer .fx-l::after { content: ""; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255,255,255,.7), transparent); }
.fx-skeleton-shimmer.is-loading .fx-skel .fx-av::after, .fx-skeleton-shimmer.is-loading .fx-l::after { animation: fx-ss-sweep 1400ms linear infinite; }
@keyframes fx-ss-sweep { to { transform: translateX(100%); } }
.fx-skeleton-shimmer .fx-content { opacity: 0; font: 13px/1.3 Inter, system-ui, sans-serif; color: #71717a; }
.fx-skeleton-shimmer .fx-content strong { color: var(--fx-ink, #18181b); font-weight: 600; }
.fx-skeleton-shimmer .fx-c1 { background: #4f46e5; } .fx-skeleton-shimmer .fx-c2 { background: #818cf8; } .fx-skeleton-shimmer .fx-c3 { background: #c7d2fe; }
.fx-skeleton-shimmer.is-loaded .fx-skel { opacity: 0; }
.fx-skeleton-shimmer.is-loaded .fx-content { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .fx-skeleton-shimmer.is-loading .fx-skel .fx-av::after, .fx-skeleton-shimmer.is-loading .fx-l::after { animation: none; } }
demo.js
export default function mount(root) {
let t;
return {
play() { clearTimeout(t); root.classList.remove('is-loaded'); root.classList.add('is-loading'); t = setTimeout(() => { root.classList.remove('is-loading'); root.classList.add('is-loaded'); }, 2200); },
stop() { clearTimeout(t); root.classList.remove('is-loading', 'is-loaded'); },
};
}
Inspired by m3.material.io — skeleton over spinner for content pages. The preview is an original re-creation; the effect name belongs to its author.
The highlight is a pseudo-element translated across each block (compositor-only), not an animated `background-position` on a wide gradient (repaints). Skeleton and content share one grid cell so the swap is a pure crossfade.
Added 2026-11-09 · Report an issue