AI CSS Animations

Map Location Skeleton

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.

HTML

<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>

CSS

.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;
  }
}

Customizing this loader

  • Color: set --ldr-color on the element or a parent. Currently previewing #3b82f6.
  • Size: set --ldr-size. Every dimension in the snippet is derived from it. Currently previewing 64px.
  • Speed: change the duration on the animation shorthand. Delays on the children are relative, so they keep their rhythm.
  • Accessibility: the root carries 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.

Similar loaders