Bonus
Skeleton loading state
Pulsing placeholders that swap for content
Pulsing placeholders that swap for content
Create a skeleton loading pattern with Tailwind CSS for a list of projects. Build the same card twice inside one bordered panel: a visible skeleton version and a hidden loaded version. The panel header has a title, a muted "Refresh" button, and a small status line. The skeleton holds four rows, each an avatar circle plus two grey bars of differing widths and a short bar on the right, all sharing animate-pulse and rounded-full bars in neutral-200 — vary the widths row to row so it does not read as a table. The loaded version holds the real rows: an initials avatar, a project name with a repository path underneath, a status chip (Passing, Running, Failed) and a relative timestamp. Then add JavaScript that swaps the skeleton for the content after a short delay to mimic a fetch, writes the loaded row count into the status line, and lets the Refresh button put the panel back into its skeleton state and reload so the transition can be seen again. <div class="mx-auto max-w-2xl p-6">
<div class="overflow-hidden rounded-2xl border border-neutral-200 bg-white">
<div class="flex items-center justify-between border-b border-neutral-100 px-5 py-4">
<div>
<h2 class="text-sm font-semibold text-neutral-900">Recent projects</h2>
<p id="loadStatus" class="text-xs text-neutral-400">Loading…</p>
</div>
<button id="reload" class="rounded-lg border border-neutral-200 px-3 py-1.5 text-xs font-medium text-neutral-600 transition hover:bg-neutral-50">Refresh</button>
</div>
<div id="skeleton" class="animate-pulse divide-y divide-neutral-100">
<div class="flex items-center gap-4 px-5 py-4">
<div class="h-9 w-9 shrink-0 rounded-full bg-neutral-200"></div>
<div class="flex-1 space-y-2"><div class="h-2.5 w-40 rounded-full bg-neutral-200"></div><div class="h-2 w-24 rounded-full bg-neutral-100"></div></div>
<div class="h-5 w-16 rounded-full bg-neutral-200"></div>
</div>
<div class="flex items-center gap-4 px-5 py-4">
<div class="h-9 w-9 shrink-0 rounded-full bg-neutral-200"></div>
<div class="flex-1 space-y-2"><div class="h-2.5 w-52 rounded-full bg-neutral-200"></div><div class="h-2 w-32 rounded-full bg-neutral-100"></div></div>
<div class="h-5 w-12 rounded-full bg-neutral-200"></div>
</div>
<div class="flex items-center gap-4 px-5 py-4">
<div class="h-9 w-9 shrink-0 rounded-full bg-neutral-200"></div>
<div class="flex-1 space-y-2"><div class="h-2.5 w-32 rounded-full bg-neutral-200"></div><div class="h-2 w-40 rounded-full bg-neutral-100"></div></div>
<div class="h-5 w-20 rounded-full bg-neutral-200"></div>
</div>
<div class="flex items-center gap-4 px-5 py-4">
<div class="h-9 w-9 shrink-0 rounded-full bg-neutral-200"></div>
<div class="flex-1 space-y-2"><div class="h-2.5 w-44 rounded-full bg-neutral-200"></div><div class="h-2 w-20 rounded-full bg-neutral-100"></div></div>
<div class="h-5 w-14 rounded-full bg-neutral-200"></div>
</div>
</div>
<div id="loaded" class="hidden divide-y divide-neutral-100">
<div class="flex items-center gap-4 px-5 py-4">
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-neutral-900 text-[11px] font-medium text-white">PA</span>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-neutral-900">payments-api</p>
<p class="truncate text-xs text-neutral-400">acme/payments-api · main</p>
</div>
<span class="rounded-full bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700">Passing</span>
<span class="w-16 text-right text-xs text-neutral-400">2m ago</span>
</div>
<div class="flex items-center gap-4 px-5 py-4">
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-[11px] font-medium text-neutral-700">WB</span>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-neutral-900">web-storefront</p>
<p class="truncate text-xs text-neutral-400">acme/web-storefront · release/4.2</p>
</div>
<span class="rounded-full bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700">Running</span>
<span class="w-16 text-right text-xs text-neutral-400">now</span>
</div>
<div class="flex items-center gap-4 px-5 py-4">
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-[11px] font-medium text-neutral-700">ET</span>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-neutral-900">events-tracker</p>
<p class="truncate text-xs text-neutral-400">acme/events-tracker · main</p>
</div>
<span class="rounded-full bg-red-50 px-2.5 py-1 text-xs font-medium text-red-700">Failed</span>
<span class="w-16 text-right text-xs text-neutral-400">18m ago</span>
</div>
<div class="flex items-center gap-4 px-5 py-4">
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-neutral-200 text-[11px] font-medium text-neutral-700">DS</span>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-neutral-900">design-system</p>
<p class="truncate text-xs text-neutral-400">acme/design-system · main</p>
</div>
<span class="rounded-full bg-green-50 px-2.5 py-1 text-xs font-medium text-green-700">Passing</span>
<span class="w-16 text-right text-xs text-neutral-400">1h ago</span>
</div>
</div>
</div>
</div> var DELAY = 1400;
var skeleton = document.getElementById("skeleton");
var loaded = document.getElementById("loaded");
var status = document.getElementById("loadStatus");
var reload = document.getElementById("reload");
function show(waiting) {
skeleton.classList.toggle("hidden", !waiting);
loaded.classList.toggle("hidden", waiting);
reload.disabled = waiting;
reload.classList.toggle("opacity-40", waiting);
status.textContent = waiting
? "Loading…"
: loaded.children.length + " projects · updated just now";
}
function load() {
show(true);
// Stand-in for the fetch — resolve yours here and render before flipping.
setTimeout(function () { show(false); }, DELAY);
}
reload.addEventListener("click", load);
load();