16 CSS Pulse Animations01 / 16

Pure CSSMIT licensed

Pulse Button CTA

A primary call-to-action button with an infinite cyan pulse ring emitting from the button edge — the standard evergreen pattern for "Sign up" / "Get started" / "Talk to sales" landing-page CTAs. Deep navy fintech palette (Stripe / Vercel / Linear landing-page convention) with electric cyan #00d4ff accent. The ring is a ::after pseudo-element animated via box-shadow outward-cast — button text stays perfectly readable through the pulse.

Published

Live Demo
Try it

The code

<div class="pl-01">
  <div class="pl-01__badge">Fintech · SaaS landing</div>
  <h2 class="pl-01__heading">Ship code, not compliance debt.</h2>
  <p class="pl-01__sub">Talk to an engineer today — get a live audit of your billing stack in 20 minutes.</p>
  <button class="pl-01__cta" type="button">
    <span>Book a call</span>
    <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <path d="M5 12h14M13 5l7 7-7 7"/>
    </svg>
  </button>
  <div class="pl-01__meta">
    <span>SOC 2 Type II</span>
    <span aria-hidden="true">·</span>
    <span>PCI DSS Level 1</span>
    <span aria-hidden="true">·</span>
    <span>99.99% uptime</span>
  </div>
</div>
.pl-01 {
  --bg: #0a1128;
  --surface: #0f1a3d;
  --ink: #f0f4ff;
  --sub: #8892b8;
  --accent: #00d4ff;
  --accent-dark: #0891b2;
  --line: rgba(240,244,255,.08);
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 56px 32px;
  background: radial-gradient(60% 90% at 50% 60%, rgba(0,212,255,.10) 0%, transparent 65%), radial-gradient(80% 100% at 50% 0%, rgba(0,212,255,.06) 0%, transparent 55%), var(--bg);
  color: var(--ink);
  text-align: center;
}

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

.pl-01 ::selection {
  background: var(--accent);
  color: var(--bg);
}

.pl-01__badge {
  display: inline-block;
  padding: 5px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(0,212,255,.06);
  color: var(--accent);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  margin-bottom: 20px;
}

.pl-01__heading {
  max-width: 520px;
  font-size: clamp(1.6rem,4vw,2.4rem);
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.1;
  margin-bottom: 12px;
}

.pl-01__sub {
  max-width: 460px;
  font-size: .95rem;
  color: var(--sub);
  line-height: 1.55;
  margin-bottom: 28px;
}

.pl-01__cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 14px 28px;
  border: none;
  border-radius: 9px;
  background: linear-gradient(180deg,var(--accent),var(--accent-dark));
  color: var(--bg);
  font-family: inherit;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -.01em;
  cursor: pointer;
  transition: transform .15s ease,box-shadow .18s ease;
  z-index: 1;
}

.pl-01__cta > span {
  position: relative;
  z-index: 1;
}

.pl-01__cta svg {
  position: relative;
  z-index: 1;
}
/* Pulse ring — box-shadow animation, compositor-only, 60fps safe */

.pl-01__cta::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 9px;
  pointer-events: none;
  z-index: 0;
  animation: pl-01-pulse 1.8s cubic-bezier(.4,0,.2,1) infinite;
}

@keyframes pl-01-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(0,212,255,.55);
  }

  70% {
    box-shadow: 0 0 0 18px rgba(0,212,255,0);
  }

  100% {
    box-shadow: 0 0 0 18px rgba(0,212,255,0);
  }
}

.pl-01__cta:hover {
  transform: translateY(-1px);
}

.pl-01__cta:active {
  transform: translateY(1px);
}

.pl-01__cta:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

.pl-01__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 24px;
  font-size: .75rem;
  color: var(--sub);
  font-weight: 500;
  letter-spacing: .02em;
}

@media (prefers-reduced-motion: reduce) {
  .pl-01__cta::after {
    animation: none!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: Pulse Button CTA
Source: https://codefronts.com/motion/css-pulse-animation/pulse-button-cta/

A primary call-to-action button with an infinite cyan pulse ring emitting from the button edge — the standard evergreen pattern for "Sign up" / "Get started" / "Talk to sales" landing-page CTAs. Deep navy fintech palette (Stripe / Vercel / Linear landing-page convention) with electric cyan #00d4ff accent. The ring is a ::after pseudo-element animated via box-shadow outward-cast — button text stays perfectly readable through the pulse.
## HTML
```html
<div class="pl-01">
  <div class="pl-01__badge">Fintech · SaaS landing</div>
  <h2 class="pl-01__heading">Ship code, not compliance debt.</h2>
  <p class="pl-01__sub">Talk to an engineer today — get a live audit of your billing stack in 20 minutes.</p>
  <button class="pl-01__cta" type="button">
    <span>Book a call</span>
    <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <path d="M5 12h14M13 5l7 7-7 7"/>
    </svg>
  </button>
  <div class="pl-01__meta">
    <span>SOC 2 Type II</span>
    <span aria-hidden="true">·</span>
    <span>PCI DSS Level 1</span>
    <span aria-hidden="true">·</span>
    <span>99.99% uptime</span>
  </div>
</div>
```
## CSS
```css
.pl-01 {
  --bg: #0a1128;
  --surface: #0f1a3d;
  --ink: #f0f4ff;
  --sub: #8892b8;
  --accent: #00d4ff;
  --accent-dark: #0891b2;
  --line: rgba(240,244,255,.08);
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 56px 32px;
  background: radial-gradient(60% 90% at 50% 60%, rgba(0,212,255,.10) 0%, transparent 65%), radial-gradient(80% 100% at 50% 0%, rgba(0,212,255,.06) 0%, transparent 55%), var(--bg);
  color: var(--ink);
  text-align: center;
}

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

.pl-01 ::selection {
  background: var(--accent);
  color: var(--bg);
}

.pl-01__badge {
  display: inline-block;
  padding: 5px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: rgba(0,212,255,.06);
  color: var(--accent);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  margin-bottom: 20px;
}

.pl-01__heading {
  max-width: 520px;
  font-size: clamp(1.6rem,4vw,2.4rem);
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.1;
  margin-bottom: 12px;
}

.pl-01__sub {
  max-width: 460px;
  font-size: .95rem;
  color: var(--sub);
  line-height: 1.55;
  margin-bottom: 28px;
}

.pl-01__cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 14px 28px;
  border: none;
  border-radius: 9px;
  background: linear-gradient(180deg,var(--accent),var(--accent-dark));
  color: var(--bg);
  font-family: inherit;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -.01em;
  cursor: pointer;
  transition: transform .15s ease,box-shadow .18s ease;
  z-index: 1;
}

.pl-01__cta > span {
  position: relative;
  z-index: 1;
}

.pl-01__cta svg {
  position: relative;
  z-index: 1;
}
/* Pulse ring — box-shadow animation, compositor-only, 60fps safe */

.pl-01__cta::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 9px;
  pointer-events: none;
  z-index: 0;
  animation: pl-01-pulse 1.8s cubic-bezier(.4,0,.2,1) infinite;
}

@keyframes pl-01-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(0,212,255,.55);
  }

  70% {
    box-shadow: 0 0 0 18px rgba(0,212,255,0);
  }

  100% {
    box-shadow: 0 0 0 18px rgba(0,212,255,0);
  }
}

.pl-01__cta:hover {
  transform: translateY(-1px);
}

.pl-01__cta:active {
  transform: translateY(1px);
}

.pl-01__cta:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

.pl-01__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 24px;
  font-size: .75rem;
  color: var(--sub);
  font-weight: 500;
  letter-spacing: .02em;
}

@media (prefers-reduced-motion: reduce) {
  .pl-01__cta::after {
    animation: none!important;
  }
}
```

How this works

The pulse effect uses a single ::after pseudo-element with the same border-radius as the button, positioned inset: 0 behind the label. A @keyframes pl-01-pulse animation cycles box-shadow from 0 0 0 0 rgba(0, 212, 255, .55) (tight ring at button edge) to 0 0 0 18px rgba(0, 212, 255, 0) (18px outward + fade to zero) over 1.8 seconds, then repeats infinitely with animation-iteration-count: infinite. Because box-shadow is a compositor-only property (no layout, no paint), the animation runs on the GPU at 60fps even when 30+ pulse buttons render on one page.

Why box-shadow not transform: scale: scaling the ::after element would scale the border-radius nonlinearly and produce a distorted ring on non-square buttons. box-shadow keeps the ring's shape perfectly matching the button's border-radius at every animation frame — the ring is always a clean rounded rectangle expanding outward.

Text sits on the button's foreground layer with z-index: 1 above the pulsing ::after. Because the pseudo-element is behind the button surface, no matter how far the pulse expands, the button label stays sharp and readable. The pulse never interferes with hover / focus states of the button — those are applied to the button element itself, not the pseudo-element.

Deep navy palette (#0a1128) with electric cyan (#00d4ff) — the fintech / SaaS landing-page convention (Stripe hero, Vercel signup, Linear "Try for free"). The vignette on the surrounding surface uses a radial-gradient centered on the button to draw the eye. Inter font with tight letter-spacing for the tech aesthetic.

Make it yours

  • Change pulse color by editing rgba(0, 212, 255, .55) in @keyframes pl-01-pulse — match your brand primary (Stripe blue #635bff, Notion black #0f0f0f, Linear purple #5e6ad2).
  • Adjust pulse speed by editing animation-duration: 1.8s — slower (2.5s+) feels more ambient, faster (1.2s) feels more urgent. Below 1s reads as "broken/glitching" and reduces click confidence.
  • Increase the pulse reach by editing the second box-shadow value 18px24px for dramatic landing-page hero CTA, 10px for a tighter form submit button.
  • For a triple-ring effect, add two more ::before pseudo-elements with staggered animation-delay: .6s and animation-delay: 1.2s — creates a radar-sweep style outward ring cascade.
  • For a click-triggered one-shot pulse (instead of infinite), swap animation-iteration-count: infinite to 1 and trigger via :active or a JS class toggle on submit.

Gotchas — read before shipping

  • The pulse ring uses box-shadow not outline or a scaled pseudo-element — outline would not respect border-radius on older Safari, and scaling a pseudo-element distorts the ring shape on non-square buttons.
  • Do NOT animate raw width, height, margin, or padding to expand the ring — every frame would trigger layout AND paint, killing 60fps and destroying INP scores on real hardware. box-shadow is compositor-only and safe.
  • The button element must have position: relative and the pseudo-element position: absolute; inset: 0 — without position: relative, the pseudo-element anchors to the closest positioned ancestor and the ring drifts off-target.
  • For accessibility, honour prefers-reduced-motion — users with vestibular disorders find infinite-loop pulses nauseating. The demo hides the pulse ring entirely when reduced-motion is preferred (button stays functional, static).
  • If the button lives inside a container with overflow: hidden, the pulse ring gets clipped at the container edge — either remove the overflow constraint or add container padding equal to the max pulse radius.
  • For dark-mode / high-contrast modes, ensure the pulse color has sufficient contrast against the surrounding surface — a cyan pulse on a dark surface is fine, but a light-cyan pulse on a light surface disappears entirely.

Browser support

ChromeSafariFirefoxEdge
45+10+40+45+

box-shadow + @keyframes. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…