16 CSS Pulse Animations10 / 16

Pure CSSMIT licensed

Map Pin Drop Pulse

A delivery / rideshare map view with a custom map pin marking a driver's current location. The pin sits on the map with a wide concentric ring pulsing on the ground layer below it — signaling "active geo-tracking, here right now." Sage map palette (#a3b18a) with cross-hatched road overlays, deep navy pin (#1e3a8a) with white shadow, and a soft ETA card overlay in the bottom corner ("7 mins away" pattern from DoorDash / Uber driver-arrived flow).

Published

Live Demo
Try it

The code

<div class="pl-10">
  <div class="pl-10__map">
    <div class="pl-10__road pl-10__road--a" aria-hidden="true"></div>
    <div class="pl-10__road pl-10__road--b" aria-hidden="true"></div>
    <div class="pl-10__vignette" aria-hidden="true"></div>
    <div class="pl-10__pin-wrap" role="img" aria-label="Driver location: 7 minutes away">
      <span class="pl-10__ground" aria-hidden="true"></span>
      <svg class="pl-10__pin" viewBox="0 0 40 56" width="40" height="56" aria-hidden="true">
        <defs>
          <linearGradient id="pl-10-pin" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stop-color="#1e40af"/>
            <stop offset="100%" stop-color="#1e3a8a"/>
          </linearGradient>
        </defs>
        <path d="M20 2 C10 2 4 10 4 20 C4 32 20 54 20 54 C20 54 36 32 36 20 C36 10 30 2 20 2 Z"
              fill="url(#pl-10-pin)" stroke="#ffffff" stroke-width="2.5"/>
        <circle cx="20" cy="20" r="6" fill="#ffffff"/>
      </svg>
    </div>
    <div class="pl-10__eta">
      <span class="pl-10__eta-avatar" aria-hidden="true">M</span>
      <div class="pl-10__eta-body">
        <p class="pl-10__eta-name">Marcus · Toyota Camry</p>
        <p class="pl-10__eta-time">
          <span class="pl-10__eta-dot" aria-hidden="true"></span>
          Arriving in <strong>7 min</strong>
        </p>
      </div>
    </div>
  </div>
</div>
.pl-10 {
  --map: #a3b18a;
  --map-dark: #8a9b73;
  --map-line: rgba(64,80,56,.14);
  --road: #dbead2;
  --road-shadow: rgba(64,80,56,.12);
  --pin: #1e3a8a;
  --pin-pulse: rgba(30,58,138,.4);
  --card: #ffffff;
  --ink: #0f172a;
  --sub: #64748b;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: block;
  padding: 0;
  background: var(--map);
  color: var(--ink);
}

.pl-10 *,
.pl-10 *::before,
.pl-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.pl-10 ::selection {
  background: var(--pin);
  color: #fff;
}

.pl-10__map {
  position: relative;
  width: 100%;
  min-height: 100vh;
  background: repeating-linear-gradient(30deg,transparent 0 30px,var(--map-line) 30px 31px), repeating-linear-gradient(60deg,transparent 0 40px,var(--map-line) 40px 41px), linear-gradient(180deg,var(--map) 0%,var(--map-dark) 100%);
  overflow: hidden;
}

.pl-10__road {
  position: absolute;
  background: var(--road);
  box-shadow: 0 0 0 1px var(--road-shadow);
}

.pl-10__road--a {
  top: 20%;
  left: -10%;
  right: -10%;
  height: 32px;
  transform: rotate(-8deg);
}

.pl-10__road--b {
  top: 5%;
  bottom: -20%;
  left: 52%;
  width: 36px;
  transform: rotate(14deg);
}

.pl-10__vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 55%, rgba(64,80,56,.32) 0%, transparent 55%);
  pointer-events: none;
}

.pl-10__pin-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-100%);
  width: 60px;
  height: 80px;
}
/* Ground pulse ring — ellipse below the pin */

.pl-10__ground {
  position: absolute;
  bottom: -4px;
  left: 50%;
  width: 80px;
  height: 22px;
  border-radius: 50%;
  background: var(--pin-pulse);
  transform: translateX(-50%);
  pointer-events: none;
  animation: pl-10-ground 2s ease-out infinite;
}

.pl-10__pin-wrap::after {
  content: '';
  position: absolute;
  bottom: -4px;
  left: 50%;
  width: 80px;
  height: 22px;
  border-radius: 50%;
  background: var(--pin-pulse);
  transform: translateX(-50%);
  pointer-events: none;
  animation: pl-10-ground 2s ease-out 1s infinite;
}

@keyframes pl-10-ground {
  0% {
    transform: translateX(-50%) scale(1);
    opacity: .55;
  }

  70% {
    transform: translateX(-50%) scale(2.2);
    opacity: 0;
  }

  100% {
    transform: translateX(-50%) scale(2.4);
    opacity: 0;
  }
}
/* Pin drop entrance */

.pl-10__pin {
  position: relative;
  z-index: 1;
  filter: drop-shadow(0 6px 8px rgba(30,58,138,.35));
  animation: pl-10-drop .5s cubic-bezier(.34,1.56,.64,1) both;
}

@keyframes pl-10-drop {
  0% {
    transform: translateY(-40px);
    opacity: 0;
  }

  100% {
    transform: translateY(0);
    opacity: 1;
  }
}

.pl-10__eta {
  position: absolute;
  bottom: 24px;
  right: 24px;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-radius: 12px;
  background: var(--card);
  box-shadow: 0 20px 40px -12px rgba(15,23,42,.25),0 4px 8px -4px rgba(15,23,42,.1);
  max-width: 280px;
}

.pl-10__eta-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  flex-shrink: 0;
  background: linear-gradient(135deg,#3b82f6,#1e40af);
  color: #fff;
  font-size: .85rem;
  font-weight: 700;
}

.pl-10__eta-name {
  font-size: .82rem;
  font-weight: 700;
  color: var(--ink);
  margin-bottom: 3px;
}

.pl-10__eta-time {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: .75rem;
  color: var(--sub);
}

.pl-10__eta-time strong {
  color: var(--ink);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.pl-10__eta-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--pin);
  animation: pl-10-blink 1.4s ease-in-out infinite;
}

@keyframes pl-10-blink {
  0%,
    100% {
    opacity: 1;
  }

  50% {
    opacity: .4;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pl-10__ground,
    .pl-10__pin-wrap::after,
    .pl-10__pin,
    .pl-10__eta-dot {
    animation: none!important;
  }

  .pl-10__pin {
    transform: translateY(0)!important;
    opacity: 1!important;
  }

  .pl-10__ground,
    .pl-10__pin-wrap::after {
    opacity: 0!important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Pulse Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Map Pin Drop Pulse
Source: https://codefronts.com/motion/css-pulse-animation/map-pin-drop-pulse/

A delivery / rideshare map view with a custom map pin marking a driver's current location. The pin sits on the map with a wide concentric ring pulsing on the ground layer below it — signaling "active geo-tracking, here right now." Sage map palette (#a3b18a) with cross-hatched road overlays, deep navy pin (#1e3a8a) with white shadow, and a soft ETA card overlay in the bottom corner ("7 mins away" pattern from DoorDash / Uber driver-arrived flow).
## HTML
```html
<div class="pl-10">
  <div class="pl-10__map">
    <div class="pl-10__road pl-10__road--a" aria-hidden="true"></div>
    <div class="pl-10__road pl-10__road--b" aria-hidden="true"></div>
    <div class="pl-10__vignette" aria-hidden="true"></div>
    <div class="pl-10__pin-wrap" role="img" aria-label="Driver location: 7 minutes away">
      <span class="pl-10__ground" aria-hidden="true"></span>
      <svg class="pl-10__pin" viewBox="0 0 40 56" width="40" height="56" aria-hidden="true">
        <defs>
          <linearGradient id="pl-10-pin" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stop-color="#1e40af"/>
            <stop offset="100%" stop-color="#1e3a8a"/>
          </linearGradient>
        </defs>
        <path d="M20 2 C10 2 4 10 4 20 C4 32 20 54 20 54 C20 54 36 32 36 20 C36 10 30 2 20 2 Z"
              fill="url(#pl-10-pin)" stroke="#ffffff" stroke-width="2.5"/>
        <circle cx="20" cy="20" r="6" fill="#ffffff"/>
      </svg>
    </div>
    <div class="pl-10__eta">
      <span class="pl-10__eta-avatar" aria-hidden="true">M</span>
      <div class="pl-10__eta-body">
        <p class="pl-10__eta-name">Marcus · Toyota Camry</p>
        <p class="pl-10__eta-time">
          <span class="pl-10__eta-dot" aria-hidden="true"></span>
          Arriving in <strong>7 min</strong>
        </p>
      </div>
    </div>
  </div>
</div>
```
## CSS
```css
.pl-10 {
  --map: #a3b18a;
  --map-dark: #8a9b73;
  --map-line: rgba(64,80,56,.14);
  --road: #dbead2;
  --road-shadow: rgba(64,80,56,.12);
  --pin: #1e3a8a;
  --pin-pulse: rgba(30,58,138,.4);
  --card: #ffffff;
  --ink: #0f172a;
  --sub: #64748b;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: block;
  padding: 0;
  background: var(--map);
  color: var(--ink);
}

.pl-10 *,
.pl-10 *::before,
.pl-10 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.pl-10 ::selection {
  background: var(--pin);
  color: #fff;
}

.pl-10__map {
  position: relative;
  width: 100%;
  min-height: 100vh;
  background: repeating-linear-gradient(30deg,transparent 0 30px,var(--map-line) 30px 31px), repeating-linear-gradient(60deg,transparent 0 40px,var(--map-line) 40px 41px), linear-gradient(180deg,var(--map) 0%,var(--map-dark) 100%);
  overflow: hidden;
}

.pl-10__road {
  position: absolute;
  background: var(--road);
  box-shadow: 0 0 0 1px var(--road-shadow);
}

.pl-10__road--a {
  top: 20%;
  left: -10%;
  right: -10%;
  height: 32px;
  transform: rotate(-8deg);
}

.pl-10__road--b {
  top: 5%;
  bottom: -20%;
  left: 52%;
  width: 36px;
  transform: rotate(14deg);
}

.pl-10__vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 55%, rgba(64,80,56,.32) 0%, transparent 55%);
  pointer-events: none;
}

.pl-10__pin-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-100%);
  width: 60px;
  height: 80px;
}
/* Ground pulse ring — ellipse below the pin */

.pl-10__ground {
  position: absolute;
  bottom: -4px;
  left: 50%;
  width: 80px;
  height: 22px;
  border-radius: 50%;
  background: var(--pin-pulse);
  transform: translateX(-50%);
  pointer-events: none;
  animation: pl-10-ground 2s ease-out infinite;
}

.pl-10__pin-wrap::after {
  content: '';
  position: absolute;
  bottom: -4px;
  left: 50%;
  width: 80px;
  height: 22px;
  border-radius: 50%;
  background: var(--pin-pulse);
  transform: translateX(-50%);
  pointer-events: none;
  animation: pl-10-ground 2s ease-out 1s infinite;
}

@keyframes pl-10-ground {
  0% {
    transform: translateX(-50%) scale(1);
    opacity: .55;
  }

  70% {
    transform: translateX(-50%) scale(2.2);
    opacity: 0;
  }

  100% {
    transform: translateX(-50%) scale(2.4);
    opacity: 0;
  }
}
/* Pin drop entrance */

.pl-10__pin {
  position: relative;
  z-index: 1;
  filter: drop-shadow(0 6px 8px rgba(30,58,138,.35));
  animation: pl-10-drop .5s cubic-bezier(.34,1.56,.64,1) both;
}

@keyframes pl-10-drop {
  0% {
    transform: translateY(-40px);
    opacity: 0;
  }

  100% {
    transform: translateY(0);
    opacity: 1;
  }
}

.pl-10__eta {
  position: absolute;
  bottom: 24px;
  right: 24px;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-radius: 12px;
  background: var(--card);
  box-shadow: 0 20px 40px -12px rgba(15,23,42,.25),0 4px 8px -4px rgba(15,23,42,.1);
  max-width: 280px;
}

.pl-10__eta-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  flex-shrink: 0;
  background: linear-gradient(135deg,#3b82f6,#1e40af);
  color: #fff;
  font-size: .85rem;
  font-weight: 700;
}

.pl-10__eta-name {
  font-size: .82rem;
  font-weight: 700;
  color: var(--ink);
  margin-bottom: 3px;
}

.pl-10__eta-time {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: .75rem;
  color: var(--sub);
}

.pl-10__eta-time strong {
  color: var(--ink);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.pl-10__eta-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--pin);
  animation: pl-10-blink 1.4s ease-in-out infinite;
}

@keyframes pl-10-blink {
  0%,
    100% {
    opacity: 1;
  }

  50% {
    opacity: .4;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pl-10__ground,
    .pl-10__pin-wrap::after,
    .pl-10__pin,
    .pl-10__eta-dot {
    animation: none!important;
  }

  .pl-10__pin {
    transform: translateY(0)!important;
    opacity: 1!important;
  }

  .pl-10__ground,
    .pl-10__pin-wrap::after {
    opacity: 0!important;
  }
}
```

How this works

The map surface uses a layered background: a base sage-green color (#a3b18a), then two repeating-linear-gradient layers at 30° and 60° with subtle darker sage lines that mimic map road crosshatches. A soft radial-gradient vignette at the pin location darkens the surrounding area slightly, drawing the eye. Two abstract "road" shapes (rounded rectangles at rotated angles) sit over the crosshatching to suggest intersecting streets — no actual map tiles needed, just the visual language of "this is a map."

The pin itself is an inverted-teardrop SVG at the center of the frame. The pulsing ground ring is a separate absolutely-positioned pseudo-element positioned at the pin's base (bottom: 0; left: 50%; transform: translateX(-50%)). It's a wide ellipse (width: 80px; height: 20px) with a subtle navy fill, animated via @keyframes pl-10-ground that cycles transform: translateX(-50%) scale(1) → scale(2.4) with opacity: .5 → 0 over 2 seconds — creating a continuous outward wave on the ground layer. A second ::after ring at half-delay (animation-delay: 1s) creates a continuous concentric wave, so as one ring fades another begins.

The pin has a small drop-in entrance animation on load — starts at translateY(-40px) + opacity: 0, drops to translateY(0) + opacity: 1 over 500ms with a bounce ease (cubic-bezier(.34, 1.56, .64, 1)). This simulates the classic map-pin "drop from above" pattern. Once landed, the pulse ring below starts its continuous cycle.

ETA card in bottom-right corner: white surface, soft shadow, avatar circle + name + ETA in JetBrains Mono. Matches DoorDash / Uber / Instacart driver-tracking UI. The card is decorative context showing the pin's real use case — a driver's location + ETA — rather than a bare pin floating in a void.

Make it yours

  • Change pin color per state — navy for driver in transit, orange for arrival warning (5 min), green for arrived, red for delayed. Map to your delivery/routing state machine.
  • Add multiple pins for multi-driver views (dispatch dashboards) — each pin gets its own pulse animation. Reserve pulse for currently-selected pin only; static dots for others.
  • For rideshare (Uber-style pickup pin + destination pin), add a second pin at a different color + shorter pulse — user distinguishes "where the car is" from "where I'm going."
  • Change the pulse ring shape from oval to circular by editing width: 80px; height: 80px; border-radius: 50% — reads as "omnidirectional signal" for sonar/tracking contexts. Oval reads as "ground shadow."
  • For high-detail maps (delivery routing with multiple waypoints), swap the CSS map surface with a real map SDK (Mapbox GL JS, Google Maps API) — the pin + pulse code stays identical, just anchor it to a map lat/lng instead of absolute positioning.

Gotchas — read before shipping

  • Map pins must ALWAYS have a real coordinate anchor in production — this demo uses position: absolute for visual layout, but production maps use lat/lng from a map SDK's projection. Never hard-code pixel positions.
  • The pulse ring should ALWAYS be BEHIND the pin (lower z-index) — if it renders on top, it obscures the pin at the ring's peak scale which reads as "pin is disappearing" instead of "actively tracking".
  • For accessibility, the pin needs role="img" with aria-label describing what it marks ("Driver location: 7 minutes away"). Screen readers can't consume visual map markers — they need explicit text.
  • The ground ring uses transform: scale which correctly preserves the oval shape. Do NOT use box-shadow — the shadow would spread as a rounded rectangle, distorting the oval into a lozenge.
  • For mobile viewports, the ETA card should slide up from bottom (not float in a corner) — matches iOS/Android map UI patterns. Consider media query with viewport-based positioning.
  • The map crosshatch background is intentionally abstract, not photorealistic — this is UI education, not a real map. For production, use a real map SDK. Never ship this CSS-only "map" to production.
  • For prefers-reduced-motion: keep the pin visible in its landed state, disable the pulse ring entirely. The information (there's a driver here) is conveyed by the pin's position alone.

Browser support

ChromeSafariFirefoxEdge
45+10+40+45+

transform + opacity + @keyframes + repeating-linear-gradient. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…