15 Pure CSS Loading Animations
Listing Card Skeleton
A premium listing-card skeleton that mirrors the real layout — photo, price, address, agent — then crossfades into the loaded card when `.ready` is added. Users see the page shape before content arrives, which is the modern, perceived-performance pattern used by Airbnb and Booking. Respects `prefers-reduced-motion`.
Listing Card Skeleton the 2nd of 15 designs in the 15 Pure CSS Loading Animations collection. The design pairs CSS styling with a small amount of JavaScript for interactivity. Copy the HTML, CSS and JavaScript panels below into your project — the JS is self-contained, has zero dependencies, and is safe to drop into any framework (React, Vue, Svelte, plain HTML). The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
The code
<div class="lc-card" aria-busy="true" aria-live="polite">
<span class="lc-img">
<span class="lc-img-shimmer" aria-hidden="true"></span>
</span>
<span class="lc-row lc-row-top">
<span class="lc-bar lc-bar-price"></span>
<span class="lc-bar lc-bar-badge"></span>
</span>
<span class="lc-bar lc-bar-line lc-bar-w-90"></span>
<span class="lc-bar lc-bar-line lc-bar-w-60"></span>
<span class="lc-row lc-row-meta">
<span class="lc-bar lc-bar-pill"></span>
<span class="lc-bar lc-bar-pill"></span>
<span class="lc-bar lc-bar-pill"></span>
</span>
<span class="lc-row lc-row-agent">
<span class="lc-avatar"></span>
<span class="lc-bar lc-bar-name"></span>
</span>
<span class="lc-loaded" aria-hidden="true">
<span class="lc-loaded-img"></span>
<span class="lc-loaded-price">£1,250,000</span>
<span class="lc-loaded-badge">For sale</span>
<span class="lc-loaded-addr">42 Oakwood Lane, Notting Hill</span>
<span class="lc-loaded-meta"><span>4 bed</span><span>3 bath</span><span>2,140 ft²</span></span>
<span class="lc-loaded-agent">
<span class="lc-loaded-avatar">SR</span>
<span class="lc-loaded-agent-name">Sarah Rowan · Boutique Estates</span>
</span>
</span>
</div> .lc-card {
position: relative;
display: grid;
gap: 8px;
width: 240px;
padding: 10px;
background: #15151d;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 14px;
font-family: system-ui, sans-serif;
}
.lc-img {
display: block;
position: relative;
width: 100%;
aspect-ratio: 5 / 3;
border-radius: 10px;
background: linear-gradient(135deg, #1f2433 0%, #2a3045 100%);
overflow: hidden;
}
.lc-img-shimmer {
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.08) 50%,
transparent 100%
);
transform: translateX(-100%);
animation: lcShimmer 1.6s ease-in-out infinite;
}
.lc-bar {
display: block;
height: 10px;
background: linear-gradient(90deg, #1f2433, #2a3045, #1f2433);
background-size: 200% 100%;
border-radius: 6px;
animation: lcPulse 1.6s ease-in-out infinite;
}
.lc-row {
display: flex;
align-items: center;
gap: 6px;
}
.lc-row-top {
justify-content: space-between;
}
.lc-bar-price {
width: 82px;
height: 16px;
}
.lc-bar-badge {
width: 56px;
height: 16px;
border-radius: 99px;
}
.lc-bar-line {
height: 9px;
}
.lc-bar-w-90 {
width: 90%;
}
.lc-bar-w-60 {
width: 60%;
}
.lc-row-meta {
gap: 5px;
}
.lc-bar-pill {
width: 48px;
height: 16px;
border-radius: 99px;
}
.lc-row-agent {
gap: 8px;
padding-top: 4px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
margin-top: 2px;
}
.lc-avatar {
display: block;
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #1f2433, #2a3045);
background-size: 200% 100%;
animation: lcPulse 1.6s ease-in-out infinite;
}
.lc-bar-name {
flex: 1;
height: 10px;
}
.lc-loaded {
position: absolute;
inset: 0;
padding: 10px;
background: #15151d;
border-radius: 14px;
display: grid;
gap: 8px;
align-content: start;
font-family: system-ui, sans-serif;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease;
}
.lc-loaded-img {
display: block;
width: 100%;
aspect-ratio: 5 / 3;
border-radius: 10px;
background:
linear-gradient(180deg, rgba(15, 15, 19, 0) 60%, rgba(15, 15, 19, 0.45) 100%),
linear-gradient(135deg, #5b8cb8 0%, #8aa6c0 35%, #d4b896 100%);
position: relative;
}
.lc-loaded-img::before {
content: "";
position: absolute;
bottom: 8px;
left: 8px;
width: 22px;
height: 22px;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 50%;
backdrop-filter: blur(6px);
}
.lc-loaded-img::after {
content: "♡";
position: absolute;
bottom: 11px;
left: 14px;
font-size: 11px;
color: #fff;
line-height: 1;
}
.lc-loaded-price {
font-size: 16px;
font-weight: 700;
color: #f0eeff;
letter-spacing: -0.01em;
display: flex;
justify-content: space-between;
align-items: center;
}
.lc-loaded-price::after {
content: "For sale";
font-size: 9px;
font-weight: 600;
background: rgba(46, 184, 138, 0.18);
color: #2eb88a;
padding: 3px 8px;
border-radius: 99px;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.lc-loaded-badge {
display: none;
}
.lc-loaded-addr {
font-size: 11px;
color: #b8b6d4;
line-height: 1.4;
}
.lc-loaded-meta {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.lc-loaded-meta span {
font-size: 9px;
font-weight: 600;
background: rgba(124, 108, 255, 0.1);
color: #a78bfa;
padding: 3px 8px;
border-radius: 99px;
letter-spacing: 0.04em;
}
.lc-loaded-agent {
display: flex;
align-items: center;
gap: 8px;
padding-top: 4px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
margin-top: 2px;
}
.lc-loaded-avatar {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #d4b896, #b89970);
color: #2a1f1a;
font-size: 9px;
font-weight: 800;
letter-spacing: 0.04em;
}
.lc-loaded-agent-name {
font-size: 10px;
color: #c8c0ff;
}
.lc-card.ready .lc-loaded {
opacity: 1;
pointer-events: auto;
}
.lc-card.ready > :not(.lc-loaded) {
opacity: 0;
}
.lc-card > :not(.lc-loaded) {
transition: opacity 0.4s ease;
}
@media (prefers-reduced-motion: reduce) {
.lc-img-shimmer,
.lc-bar,
.lc-avatar {
animation: none;
}
}
@keyframes lcShimmer {
100% {
transform: translateX(100%);
}
}
@keyframes lcPulse {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
} // Toggles the .ready state to crossfade skeleton -> loaded card.
// Replace this loop with your real "data fetched" trigger.
document.querySelectorAll(".lc-card").forEach(function (card) {
var ready = false;
function tick() {
ready = !ready;
card.classList.toggle("ready", ready);
card.setAttribute("aria-busy", ready ? "false" : "true");
setTimeout(tick, ready ? 2200 : 2800);
}
setTimeout(tick, 2800);
});