20 CSS Animated Buttons 03 / 20
CSS Pure CSS Ripple Effect Button
A Material Design-inspired ripple effect on button click, achieved entirely with CSS pseudo-elements and the :active state — no JavaScript required.
The code
<div class="ab-03">
<p class="ab-03__label">Click or tap to trigger</p>
<div class="ab-03__row">
<button class="ab-03__btn ab-03__btn--violet">Get Started</button>
<button class="ab-03__btn ab-03__btn--teal">Learn More</button>
<button class="ab-03__btn ab-03__btn--rose">Subscribe</button>
</div>
</div> <div class="ab-03">
<p class="ab-03__label">Click or tap to trigger</p>
<div class="ab-03__row">
<button class="ab-03__btn ab-03__btn--violet">Get Started</button>
<button class="ab-03__btn ab-03__btn--teal">Learn More</button>
<button class="ab-03__btn ab-03__btn--rose">Subscribe</button>
</div>
</div>.ab-03,.ab-03 *,.ab-03 *::before,.ab-03 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-03 ::selection{background:#7c3aed;color:#fff}
.ab-03{
font-family:system-ui,sans-serif;
background:#0f0c1a;
min-height:100vh;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:2rem;
padding:2rem;
}
.ab-03__label{
font-size:.78rem;
letter-spacing:.14em;
text-transform:uppercase;
color:#a78bfa;
opacity:.6;
}
.ab-03__row{
display:flex;
gap:1rem;
flex-wrap:wrap;
justify-content:center;
}
.ab-03__btn{
position:relative;
overflow:hidden;
padding:.85rem 2rem;
font-size:.95rem;
font-weight:700;
color:#fff;
border:none;
border-radius:10px;
cursor:pointer;
outline:none;
letter-spacing:.03em;
transition:filter .2s;
}
.ab-03__btn:hover{filter:brightness(1.1)}
.ab-03__btn:active{filter:brightness(.9)}
.ab-03__btn--violet{background:#7c3aed}
.ab-03__btn--teal{background:#0d9488}
.ab-03__btn--rose{background:#e11d48}
.ab-03__btn::after{
content:'';
position:absolute;
top:50%;left:50%;
width:0;height:0;
background:rgba(255,255,255,.4);
border-radius:50%;
transform:translate(-50%,-50%);
pointer-events:none;
opacity:0;
}
.ab-03__btn:active::after{
animation:ab-03-ripple .6s linear forwards;
}
@keyframes ab-03-ripple{
0%{width:0;height:0;opacity:.4}
80%{width:400%;height:400%;opacity:.1}
100%{width:500%;height:500%;opacity:0}
}
@media(prefers-reduced-motion:reduce){
.ab-03__btn:active::after{animation:none}
} .ab-03,.ab-03 *,.ab-03 *::before,.ab-03 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-03 ::selection{background:#7c3aed;color:#fff}
.ab-03{
font-family:system-ui,sans-serif;
background:#0f0c1a;
min-height:100vh;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:2rem;
padding:2rem;
}
.ab-03__label{
font-size:.78rem;
letter-spacing:.14em;
text-transform:uppercase;
color:#a78bfa;
opacity:.6;
}
.ab-03__row{
display:flex;
gap:1rem;
flex-wrap:wrap;
justify-content:center;
}
.ab-03__btn{
position:relative;
overflow:hidden;
padding:.85rem 2rem;
font-size:.95rem;
font-weight:700;
color:#fff;
border:none;
border-radius:10px;
cursor:pointer;
outline:none;
letter-spacing:.03em;
transition:filter .2s;
}
.ab-03__btn:hover{filter:brightness(1.1)}
.ab-03__btn:active{filter:brightness(.9)}
.ab-03__btn--violet{background:#7c3aed}
.ab-03__btn--teal{background:#0d9488}
.ab-03__btn--rose{background:#e11d48}
.ab-03__btn::after{
content:'';
position:absolute;
top:50%;left:50%;
width:0;height:0;
background:rgba(255,255,255,.4);
border-radius:50%;
transform:translate(-50%,-50%);
pointer-events:none;
opacity:0;
}
.ab-03__btn:active::after{
animation:ab-03-ripple .6s linear forwards;
}
@keyframes ab-03-ripple{
0%{width:0;height:0;opacity:.4}
80%{width:400%;height:400%;opacity:.1}
100%{width:500%;height:500%;opacity:0}
}
@media(prefers-reduced-motion:reduce){
.ab-03__btn:active::after{animation:none}
}How this works
The ripple lives in the ::after pseudo-element, which starts as a small circle at the button centre using border-radius: 50% and width/height: 0. On :active, a @keyframes ab-03-ripple animation scales the circle to 400% of the button width while fading opacity from 0.35 to 0 — the classic expanding ring illusion. Centering the pseudo-element with top: 50%; left: 50%; transform: translate(-50%,-50%) keeps the ripple origin consistent regardless of button dimensions.
Because the animation only fires on :active, it naturally resets when the user releases the pointer. overflow: hidden on the parent clips the expanding disk, while pointer-events: none on the pseudo-element prevents it from interfering with the click target. The entire effect uses only transform, opacity, and width/height inside the keyframe, with only the transform path on the compositor — width/height changes trigger layout but are brief and isolated to the pseudo-element.
Customize
- Reposition the ripple origin to a corner by changing the
top/leftpercentages on::after— e.g.top: 80%; left: 10%for a bottom-left burst. - Control ripple speed by editing the
animation-durationon.ab-03__btn:active::after(default.6s; try.4sfor snappy feel). - Change the ripple colour by updating the
backgroundon.ab-03__btn::after(defaultrgba(255,255,255,.45)). - Add a fill-colour variant by appending a second keyframe that changes the button
backgroundon active, creating a ripple-plus-fill combo. - Give dark-coloured buttons a dark ripple by switching the pseudo-element background to
rgba(0,0,0,.12)so it reads on light fill colours.
Watch out for
- The pure-CSS ripple always originates from the element centre — it cannot track where the user clicked. Use a JS-powered version if accurate origin matters for the design.
- On iOS Safari,
:activeonly fires if the element has acursor: pointeror a touch event listener — addcursor:pointerto the button to ensure activation on mobile. - If the button has
border-radius > 50%(a pill shape), the escaping corners of the growing::aftercircle must be clipped withoverflow: hiddenon the button.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 26+ | 7+ | 16+ | 26+ |
The :active pseudo-class and CSS animations are universally supported across all modern browsers.