26 CSS Link Effects14 / 26

Pure CSSMIT licensed

CSS Pulse / Heartbeat Link Hover Effect

Three tempos of alive: a radar ping that radiates rings from a notification link, an anatomically-timed heartbeat (lub-DUB, then rest — not a metronome sine wave) on a like chip, and a slow breathing glow for a primary CTA. Each maps to a different UI job: attention, affection, invitation.

Published Updated

Live Demo
Try it

The code

<section class="cle-14" aria-label="Pulse and heartbeat hover demo">
  <div class="cle-14__stage">
    <div class="cle-14__var"><a href="#a" class="cle-14__ping"><span class="cle-14__dot" aria-hidden="true"></span>3 new reviews</a><small class="cle-14__cap">A · radar ping</small></div>
    <div class="cle-14__var"><a href="#b" class="cle-14__beat"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20.3 4.6 13a4.9 4.9 0 0 1 0-7 4.9 4.9 0 0 1 7 0l.4.4.4-.4a4.9 4.9 0 0 1 7 0 4.9 4.9 0 0 1 0 7z"/></svg>Save to favorites</a><small class="cle-14__cap">B · lub-dub heartbeat</small></div>
    <div class="cle-14__var"><a href="#c" class="cle-14__breathe">Start free trial</a><small class="cle-14__cap">C · breathing glow</small></div>
  </div>
</section>
.cle-14,
.cle-14 *,
.cle-14 *::before,
.cle-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-14 {
  width: 100%;
  min-height: 100vh;
  --ink: oklch(0.25 0.02 20);
  --brand: oklch(0.62 0.21 20);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-14__stage {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 44px 56px;
  border: 1px solid oklch(0.91 0.015 30);
  background: linear-gradient(170deg,oklch(0.98 0.008 40),oklch(0.95 0.018 25));
  padding: 44px 36px;
}

.cle-14__var {
  display: grid;
  gap: 14px;
  justify-items: center;
}

.cle-14__cap {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: oklch(0.55 0.04 25);
}

.cle-14 a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 26px;
  border-radius: 999px;
  background: oklch(1 0 0 / .85);
  border: 1.5px solid oklch(0.88 0.03 25);
  color: var(--ink);
  text-decoration: none;
  font-size: 15.5px;
  font-weight: 650;
  transition: border-color .3s,color .3s;
  box-shadow: 0 10px 26px -20px oklch(0.4 0.1 20 / .7);
}

.cle-14 a:hover,
.cle-14 a:focus-visible {
  border-color: var(--brand);
  color: oklch(0.45 0.19 20);
}

.cle-14 a:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 3px;
}

.cle-14__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--brand);
}

.cle-14__ping::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  border: 2px solid var(--brand);
  opacity: 0;
  pointer-events: none;
}

.cle-14__ping:hover::after,
.cle-14__ping:focus-visible::after {
  animation: clh14-ping 1.1s cubic-bezier(0,.5,.4,1) infinite;
}

@keyframes clh14-ping {
  from {
    scale: 1;
    opacity: .8;
  }

  to {
    scale: 1.45;
    opacity: 0;
  }
}

.cle-14__beat svg {
  width: 19px;
  height: 19px;
  fill: var(--brand);
  stroke: none;
}

.cle-14__beat:hover svg,
.cle-14__beat:focus-visible svg {
  animation: clh14-beat 1.3s ease-in-out infinite;
}

@keyframes clh14-beat {
  0% {
    scale: 1;
  }

  12% {
    scale: 1.22;
  }

  24% {
    scale: 1;
  }

  34% {
    scale: 1.14;
  }

  45%,
    100% {
    scale: 1;
  }
}

.cle-14__breathe {
  background: linear-gradient(150deg,var(--brand),oklch(0.5 0.2 350));
  border-color: transparent;
  color: oklch(0.99 0.005 20);
}

.cle-14__breathe:hover,
.cle-14__breathe:focus-visible {
  color: #fff;
  animation: clh14-breathe 1.8s ease-in-out infinite alternate;
}

@keyframes clh14-breathe {
  from {
    box-shadow: 0 6px 18px -8px color-mix(in oklch,var(--brand),transparent 30%);
    scale: 1;
  }

  to {
    box-shadow: 0 10px 34px -6px color-mix(in oklch,var(--brand),transparent 12%);
    scale: 1.015;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cle-14 * {
    animation: none !important;
    transition-duration: .01s !important;
  }

  .cle-14__breathe:hover {
    box-shadow: 0 10px 30px -8px color-mix(in oklch,var(--brand),transparent 20%);
  }
}
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 Link Effect 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: CSS Pulse / Heartbeat Link Hover Effect
Source: https://codefronts.com/motion/css-link-effects/heartbeat-pulse/

Three tempos of alive: a radar ping that radiates rings from a notification link, an anatomically-timed heartbeat (lub-DUB, then rest — not a metronome sine wave) on a like chip, and a slow breathing glow for a primary CTA. Each maps to a different UI job: attention, affection, invitation.
## HTML
```html
<section class="cle-14" aria-label="Pulse and heartbeat hover demo">
  <div class="cle-14__stage">
    <div class="cle-14__var"><a href="#a" class="cle-14__ping"><span class="cle-14__dot" aria-hidden="true"></span>3 new reviews</a><small class="cle-14__cap">A · radar ping</small></div>
    <div class="cle-14__var"><a href="#b" class="cle-14__beat"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20.3 4.6 13a4.9 4.9 0 0 1 0-7 4.9 4.9 0 0 1 7 0l.4.4.4-.4a4.9 4.9 0 0 1 7 0 4.9 4.9 0 0 1 0 7z"/></svg>Save to favorites</a><small class="cle-14__cap">B · lub-dub heartbeat</small></div>
    <div class="cle-14__var"><a href="#c" class="cle-14__breathe">Start free trial</a><small class="cle-14__cap">C · breathing glow</small></div>
  </div>
</section>
```
## CSS
```css
.cle-14,
.cle-14 *,
.cle-14 *::before,
.cle-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cle-14 {
  width: 100%;
  min-height: 100vh;
  --ink: oklch(0.25 0.02 20);
  --brand: oklch(0.62 0.21 20);
  font-family: 'Segoe UI',system-ui,sans-serif;
}

.cle-14__stage {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 44px 56px;
  border: 1px solid oklch(0.91 0.015 30);
  background: linear-gradient(170deg,oklch(0.98 0.008 40),oklch(0.95 0.018 25));
  padding: 44px 36px;
}

.cle-14__var {
  display: grid;
  gap: 14px;
  justify-items: center;
}

.cle-14__cap {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: oklch(0.55 0.04 25);
}

.cle-14 a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 26px;
  border-radius: 999px;
  background: oklch(1 0 0 / .85);
  border: 1.5px solid oklch(0.88 0.03 25);
  color: var(--ink);
  text-decoration: none;
  font-size: 15.5px;
  font-weight: 650;
  transition: border-color .3s,color .3s;
  box-shadow: 0 10px 26px -20px oklch(0.4 0.1 20 / .7);
}

.cle-14 a:hover,
.cle-14 a:focus-visible {
  border-color: var(--brand);
  color: oklch(0.45 0.19 20);
}

.cle-14 a:focus-visible {
  outline: 3px solid var(--brand);
  outline-offset: 3px;
}

.cle-14__dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--brand);
}

.cle-14__ping::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  border: 2px solid var(--brand);
  opacity: 0;
  pointer-events: none;
}

.cle-14__ping:hover::after,
.cle-14__ping:focus-visible::after {
  animation: clh14-ping 1.1s cubic-bezier(0,.5,.4,1) infinite;
}

@keyframes clh14-ping {
  from {
    scale: 1;
    opacity: .8;
  }

  to {
    scale: 1.45;
    opacity: 0;
  }
}

.cle-14__beat svg {
  width: 19px;
  height: 19px;
  fill: var(--brand);
  stroke: none;
}

.cle-14__beat:hover svg,
.cle-14__beat:focus-visible svg {
  animation: clh14-beat 1.3s ease-in-out infinite;
}

@keyframes clh14-beat {
  0% {
    scale: 1;
  }

  12% {
    scale: 1.22;
  }

  24% {
    scale: 1;
  }

  34% {
    scale: 1.14;
  }

  45%,
    100% {
    scale: 1;
  }
}

.cle-14__breathe {
  background: linear-gradient(150deg,var(--brand),oklch(0.5 0.2 350));
  border-color: transparent;
  color: oklch(0.99 0.005 20);
}

.cle-14__breathe:hover,
.cle-14__breathe:focus-visible {
  color: #fff;
  animation: clh14-breathe 1.8s ease-in-out infinite alternate;
}

@keyframes clh14-breathe {
  from {
    box-shadow: 0 6px 18px -8px color-mix(in oklch,var(--brand),transparent 30%);
    scale: 1;
  }

  to {
    box-shadow: 0 10px 34px -6px color-mix(in oklch,var(--brand),transparent 12%);
    scale: 1.015;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cle-14 * {
    animation: none !important;
    transition-duration: .01s !important;
  }

  .cle-14__breathe:hover {
    box-shadow: 0 10px 30px -8px color-mix(in oklch,var(--brand),transparent 20%);
  }
}
```

How this works

The ping (A) is a pseudo-element clone of the pill's outline that inflates and fades on a loop while hovered:

.ping::after{ content:""; position:absolute; inset:0;
  border-radius:inherit; border:2px solid var(--brand); opacity:0; }
.ping:hover::after{ animation: clh14-ping 1.1s cubic-bezier(0,.5,.4,1) infinite; }
@keyframes clh14-ping{ from{ scale:1; opacity:.8 } to{ scale:1.45; opacity:0 } }

border-radius:inherit keeps the ring pill-shaped, and starting the fade from 0.8 (not 1) stops the ring reading as a second border on frame one.

The heartbeat (B) earns its name with real cardiac timing — two contractions of different strength packed into the first 45% of the cycle, then a long diastole rest: 0% 1 → 12% 1.22 → 24% 1 → 34% 1.14 → 45% 1 → 100% 1. Only the heart glyph scales; the chip stays still so the text remains readable. C animates a layered box-shadow bloom with alternate direction and an ease-in-out curve — a 1.8s inhale/exhale, subtle enough to survive being next to real content.

Make it yours

  • Ping ring count: stack a second delayed ring on ::before at animation-delay:.55s for the double-radar look.
  • Heart rate: B's 1.3s loop ≈ 46bpm hover-calm; drop to 1s for urgency.
  • Breathing depth: C's shadow spread pair (12px→22px) and the scale(1.015) ceiling.
  • All three idle statically — move the animation to the base selector for permanent ambient motion (sparingly).

Gotchas — read before shipping

  • Infinite scale on text blurs subpixel rendering in some engines — B scales only the glyph span, never the label.
  • Keep ping opacity starting below 1 and the ring border thin; a heavy ring reads as a focus indicator and confuses keyboard users.
  • prefers-reduced-motion: all three collapse to a static color/shadow emphasis (shipped) — pulse conveys nothing essential, so it can simply stop.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

Keyframed transform/opacity/box-shadow is universal; individual scale property needs Chrome 104 / Safari 14.1 / Firefox 72. oklch/color-mix set the stated floor.

Search CodeFronts

Loading…