16 CSS Pulse Animations01 / 16
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
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><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;
}
}.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;
}
}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;
}
}
```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
18px—24pxfor dramatic landing-page hero CTA,10pxfor a tighter form submit button. - For a triple-ring effect, add two more
::beforepseudo-elements with staggeredanimation-delay: .6sandanimation-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: infiniteto1and trigger via:activeor a JS class toggle on submit.
Gotchas — read before shipping
- The pulse ring uses
box-shadownotoutlineor a scaled pseudo-element —outlinewould 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, orpaddingto expand the ring — every frame would trigger layout AND paint, killing 60fps and destroying INP scores on real hardware.box-shadowis compositor-only and safe. - The button element must have
position: relativeand the pseudo-elementposition: absolute; inset: 0— withoutposition: 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 45+ | 10+ | 40+ | 45+ |
box-shadow + @keyframes. Universal, GPU-accelerated.