A location card: a placeholder map with roads and a park, a pin that drops in, squashes and sends out a ripple, then address lines and a directions button. The map is all layered gradients.
Also known as: map skeleton loader, store locator placeholder, location card skeleton.
<div class="ldr-skeleton-map-card" role="status" aria-label="Loading content"> <div class="ldr-skeleton-map-card-map"><span class="ldr-skeleton-map-card-pin"></span></div> <div class="ldr-skeleton-map-card-info"><span></span><span></span><span></span><span class="ldr-skeleton-map-card-go"></span></div> </div>
.ldr-skeleton-map-card {
--s: var(--ldr-size, 48px);
--c: var(--ldr-color, #3b82f6);
width: calc(var(--s) * 5);
overflow: hidden;
border: 1px solid color-mix(in srgb, var(--ldr-color, #3b82f6) 18%, transparent);
border-radius: calc(var(--s) / 6);
}
.ldr-skeleton-map-card-map {
position: relative;
height: calc(var(--s) * 1.6);
background:
linear-gradient(transparent 60%, color-mix(in srgb, var(--ldr-color, #3b82f6) 22%, transparent) 0 calc(60% + var(--s) / 10), transparent 0),
linear-gradient(90deg, transparent 28%, color-mix(in srgb, var(--ldr-color, #3b82f6) 18%, transparent) 0 calc(28% + var(--s) / 14), transparent 0),
linear-gradient(118deg, transparent 62%, color-mix(in srgb, var(--ldr-color, #3b82f6) 22%, transparent) 0 calc(62% + var(--s) / 8), transparent 0),
radial-gradient(closest-side, color-mix(in srgb, var(--ldr-color, #3b82f6) 18%, transparent) 70%, transparent) 88% 12% / calc(var(--s) * 1.3) calc(var(--s) * 0.8) no-repeat,
radial-gradient(closest-side, color-mix(in srgb, var(--ldr-color, #3b82f6) 14%, transparent) 70%, transparent) 6% 100% / calc(var(--s) * 1) calc(var(--s) * 0.7) no-repeat,
color-mix(in srgb, var(--ldr-color, #3b82f6) 8%, transparent);
}
.ldr-skeleton-map-card-pin {
position: absolute;
left: 50%;
top: 22%;
width: calc(var(--s) * 0.4);
height: calc(var(--s) * 0.4);
margin-left: calc(var(--s) * -0.2);
transform-origin: 50% 130%;
animation: ldr-skeleton-map-card-drop 2.6s infinite;
}
.ldr-skeleton-map-card-pin::before {
content: "";
position: absolute;
inset: 0;
border-radius: 50% 50% 50% 0;
background: radial-gradient(circle, color-mix(in srgb, var(--c) 30%, #fff) 0 22%, var(--c) 25%);
transform: rotate(-45deg);
}
.ldr-skeleton-map-card-map::after {
content: "";
position: absolute;
left: 50%;
top: calc(22% + var(--s) * 0.5);
width: calc(var(--s) * 0.9);
height: calc(var(--s) * 0.3);
margin: calc(var(--s) * -0.15) 0 0 calc(var(--s) * -0.45);
border-radius: 50%;
box-shadow: inset 0 0 0 max(1px, calc(var(--s) / 30)) var(--c);
opacity: 0;
animation: ldr-skeleton-map-card-ripple 2.6s ease-out infinite;
}
.ldr-skeleton-map-card-info {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: calc(var(--s) / 9) calc(var(--s) / 4);
padding: calc(var(--s) / 5);
}
.ldr-skeleton-map-card-info span {
grid-column: 1;
height: calc(var(--s) / 8);
border-radius: var(--s);
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 18%, transparent);
animation: ldr-skeleton-map-card-breathe 2.6s ease-in-out infinite;
}
.ldr-skeleton-map-card-info span:nth-child(1) { width: 58%; height: calc(var(--s) / 5.5); margin-bottom: calc(var(--s) / 20); background: color-mix(in srgb, var(--ldr-color, #3b82f6) 26%, transparent); }
.ldr-skeleton-map-card-info span:nth-child(2) { width: 88%; }
.ldr-skeleton-map-card-info span:nth-child(3) { width: 46%; }
.ldr-skeleton-map-card-info .ldr-skeleton-map-card-go {
grid-area: 1 / 2 / 4 / 3;
display: grid;
place-items: center;
width: calc(var(--s) * 0.6);
height: calc(var(--s) * 0.6);
border-radius: 50%;
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 20%, transparent);
animation: none;
}
.ldr-skeleton-map-card-go::before {
content: "";
width: calc(var(--s) * 0.24);
height: calc(var(--s) * 0.24);
background: color-mix(in srgb, var(--ldr-color, #3b82f6) 55%, transparent);
clip-path: polygon(0 45%, 100% 0, 55% 100%, 48% 52%);
}
@keyframes ldr-skeleton-map-card-drop {
0% { transform: translateY(calc(var(--s) * -0.9)); opacity: 0; animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45); }
8% { opacity: 1; }
20% { transform: translateY(0) scale(1.18, 0.8); animation-timing-function: cubic-bezier(0.2, 0.8, 0.4, 1); }
28% { transform: translateY(calc(var(--s) * -0.16)) scale(0.94, 1.08); animation-timing-function: cubic-bezier(0.6, 0, 0.8, 0.4); }
36% { transform: translateY(0) scale(1.06, 0.94); animation-timing-function: ease-out; }
42%, 86% { transform: none; opacity: 1; }
100% { transform: translateY(calc(var(--s) * -0.2)); opacity: 0; }
}
@keyframes ldr-skeleton-map-card-ripple {
0%, 19% { transform: scale(0.2); opacity: 0; }
21% { transform: scale(0.3); opacity: 1; }
60% { transform: scale(1.4); opacity: 0; }
100% { transform: scale(1.4); opacity: 0; }
}
@keyframes ldr-skeleton-map-card-breathe {
50% { opacity: 0.5; }
}
@media (prefers-reduced-motion: reduce) {
.ldr-skeleton-map-card, .ldr-skeleton-map-card *, .ldr-skeleton-map-card::before, .ldr-skeleton-map-card::after, .ldr-skeleton-map-card *::before, .ldr-skeleton-map-card *::after {
animation-duration: 3s;
}
}--ldr-color on the element or a parent.
Currently previewing #3b82f6.--ldr-size. Every dimension in the snippet is
derived from it. Currently previewing 64px.animation shorthand.
Delays on the children are relative, so they keep their rhythm.role="status" and an
aria-label so screen readers announce the wait. The reduced-motion rule
at the end slows the animation for users who asked for less motion.