Pure CSS Scenes & objects #loop #map #pin #billboard #scene

Map pins in pure CSS

A map drawn with gradients lies on a tilted plane. The pins undo the plane’s rotation around their tip, so they stand up and face you, then bounce in turn above a pulsing ring.

  • 138 lines of CSS
  • 9 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The map is one element painted with layered gradients (land ellipses, a river cut from a big ring, two roads, a grid), tilted with rotateX(54deg) rotateZ(-14deg).
  2. Each pin anchor is a zero-size point on the map. Its pulse ring is a child of that point, so it stays flat in the ground plane and spreads as an ellipse.
  3. The pin undoes the plane in reverse order, rotateZ(14deg) rotateX(-54deg), around transform-origin: 50% 100%: its tip. It stands up exactly on its spot and faces you.
  4. Bounce keyframes repeat that inverse and add translateY and a squash scale after it, so the hop happens in screen space. animation-delay: calc(var(--i) * 0.8s) makes the pins take turns.
  5. Every length is a multiple of one base unit, --u, tied to the canvas: the map is 190 × 150 units and a pin 26 wide, so the scene is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • map from layered gradients
  • pin: inverse rotation, transform-origin bottom
  • ring stays in the ground plane
  • staggered animation-delay

Copy-paste code

HTML 9 lines

<div class="scene">
  <div class="map">
    <div class="plane">
      <i style="--x:24%;--y:34%;--c:#ff4d9d;--i:0"><b></b></i>
      <i style="--x:58%;--y:66%;--c:#ffb547;--i:1"><b></b></i>
      <i style="--x:80%;--y:28%;--c:#8b6cff;--i:2"><b></b></i>
    </div>
  </div>
</div>

CSS 138 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the map is the same share of a
     gallery card, the editor, a full screen and a recording canvas */
  --u: 0.4vmin;
  display: grid;
  place-items: center;
  perspective: calc(800 * var(--u));
}

.map {
  position: relative;
  width: calc(210 * var(--u));
  height: calc(180 * var(--u));
  transform-style: preserve-3d;
}

.plane {
  box-sizing: content-box; /* its size is its inside, its edge outside it, on any page */
  --land: #1c5a60;
  --water: #262a55;
  --road: rgb(255 181 71 / 0.6);
  position: absolute;
  left: calc(10 * var(--u));
  top: calc(18 * var(--u));
  width: calc(190 * var(--u));
  height: calc(150 * var(--u));
  border: calc(1 * var(--u)) solid rgb(46 230 214 / 0.45);
  border-radius: calc(14 * var(--u));
  background:
    linear-gradient(28deg, transparent calc(50% - calc(1.5 * var(--u))), var(--road) 0 calc(50% + calc(1.5 * var(--u))), transparent 0),
    linear-gradient(90deg, transparent 63%, var(--road) 0 calc(63% + calc(3 * var(--u))), transparent 0),
    radial-gradient(circle at 115% 125%, transparent 56%, var(--water) 0 59%, transparent 0),
    radial-gradient(ellipse 30% 25% at 26% 32%, var(--land) 98%, transparent),
    radial-gradient(ellipse 26% 22% at 66% 68%, var(--land) 98%, transparent),
    radial-gradient(ellipse 15% 13% at 82% 24%, var(--land) 98%, transparent),
    radial-gradient(ellipse 12% 10% at 26% 80%, var(--land) 98%, transparent),
    linear-gradient(90deg, rgb(236 238 251 / 0.07) calc(1 * var(--u)), transparent calc(1 * var(--u))) 0 0 / calc(19 * var(--u)) calc(19 * var(--u)),
    linear-gradient(rgb(236 238 251 / 0.07) calc(1 * var(--u)), transparent calc(1 * var(--u))) 0 0 / calc(19 * var(--u)) calc(19 * var(--u)),
    var(--water);
  transform-style: preserve-3d;
  transform: rotateX(54deg) rotateZ(-14deg);
}

/* the anchor: a point on the map, one unit above it */
.plane i {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  transform: translateZ(calc(1 * var(--u)));
}

.plane i::before,
.plane i::after {
  content: '';
  position: absolute;
  border-radius: 50%;
  animation: 2.4s infinite;
  animation-delay: calc(var(--i) * 0.8s);
}

/* pulse ring, flat on the ground */
.plane i::before {
  top: calc(-16 * var(--u));
  left: calc(-16 * var(--u));
  width: calc(32 * var(--u));
  height: calc(32 * var(--u));
  border: calc(2 * var(--u)) solid var(--c);
  box-sizing: border-box;
  opacity: 0;
  animation-name: ring;
  animation-timing-function: ease-out;
}

/* contact shadow */
.plane i::after {
  top: calc(-4 * var(--u));
  left: calc(-7 * var(--u));
  width: calc(14 * var(--u));
  height: calc(8 * var(--u));
  background: rgb(0 0 0 / 0.45);
  animation-name: shadow;
  animation-timing-function: ease-in-out;
}

/* the pin: tip at bottom centre = transform-origin */
.plane b {
  position: absolute;
  bottom: 0;
  left: calc(-13 * var(--u));
  width: calc(26 * var(--u));
  height: calc(32 * var(--u));
  transform-origin: 50% 100%;
  transform: rotateZ(14deg) rotateX(-54deg); /* the plane, undone */
  animation: bounce 2.4s ease-in-out infinite;
  animation-delay: calc(var(--i) * 0.8s);
}

/* head: three round corners, turned so the sharp one points down */
.plane b::before {
  content: '';
  position: absolute;
  width: calc(26 * var(--u));
  height: calc(26 * var(--u));
  border-radius: 50% 50% 50% 0;
  background: radial-gradient(circle at 70% 30%, color-mix(in srgb, var(--c) 40%, #fff), var(--c) 55%, color-mix(in srgb, var(--c) 60%, #000));
  transform: rotate(-45deg);
}

.plane b::after {
  content: '';
  position: absolute;
  top: calc(8 * var(--u));
  left: calc(8 * var(--u));
  width: calc(10 * var(--u));
  height: calc(10 * var(--u));
  border-radius: 50%;
  background: #fff;
}

@keyframes bounce {
  0%, 36%, 100% { transform: rotateZ(14deg) rotateX(-54deg) translateY(0) scale(1, 1); }
  14%           { transform: rotateZ(14deg) rotateX(-54deg) translateY(calc(-16 * var(--u))) scale(0.94, 1.06); }
  26%           { transform: rotateZ(14deg) rotateX(-54deg) translateY(0) scale(1.14, 0.86); }
}

@keyframes shadow {
  0%, 36%, 100% { opacity: 1; transform: scale(1); }
  14%           { opacity: 0.45; transform: scale(0.6); }
}

@keyframes ring {
  0%, 24%   { opacity: 0; transform: scale(0.2); }
  27%       { opacity: 1; transform: scale(0.3); }
  70%, 100% { opacity: 0; transform: scale(1.5); }
}
Standalone snippet · plain CSS, no build step, no dependencies.