30 CSS Hover Effects12 / 30

Pure CSSMIT licensed

CSS Ripple Button Hover Effect

Five ripple and pulse hover effects on buttons — center-out ring, double ripple, contained wave, border-pulse, and glow-ring expansion — all using pseudo-element scale animations and opacity keyframes to simulate a water-droplet impact.

Published

Live Demo
Try it

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

The code

<div class="hv-12">
  <div class="hv-12__stack">
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--ring">Center Ripple</button>
      <span class="hv-12__label">scale + opacity keyframe</span>
    </div>
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--double">Double Ring</button>
      <span class="hv-12__label">staggered dual ripple</span>
    </div>
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--wave">Contained Wave</button>
      <span class="hv-12__label">overflow-clipped wave</span>
    </div>
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--pulse">Border Pulse</button>
      <span class="hv-12__label">outline-offset ping</span>
    </div>
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--glow">Glow Ring</button>
      <span class="hv-12__label">box-shadow expand</span>
    </div>
  </div>
</div>
.hv-12,
.hv-12 *,
.hv-12 *::before,
.hv-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.hv-12 ::selection {
  background: #be185d;
  color: #fff;
}

.hv-12 {
  --bg: #0f0008;
  --text: #fce7f3;
  --dim: #6b7280;
  --pink: #ec4899;
  --rose: #f43f5e;
  --magenta: #db2777;
  --purple: #a855f7;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 60px 24px;
}

.hv-12__stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: min(640px,100%);
}

.hv-12__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 24px 36px;
  background: rgba(255,255,255,.025);
  border: 1px solid rgba(255,255,255,.07);
  border-radius: 12px;
}

.hv-12__label {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
  flex-shrink: 0;
}
/* shared btn */

.hv-12__btn {
  position: relative;
  padding: 13px 36px;
  font-size: .95rem;
  font-weight: 600;
  letter-spacing: .05em;
  cursor: pointer;
  border-radius: 8px;
  border: none;
  flex-shrink: 0;
}
/* 1 — center ripple */

.hv-12__btn--ring {
  background: var(--pink);
  color: #fff;
  overflow: visible;
}

.hv-12__btn--ring::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: rgba(255,255,255,.4);
  transform: translate(-50%,-50%) scale(0);
  pointer-events: none;
}

.hv-12__btn--ring:hover::after {
  animation: hv-12-ripple .6s ease-out forwards;
}

@keyframes hv-12-ripple {
  0% {
    transform: translate(-50%,-50%) scale(0);
    opacity: .7;
  }

  100% {
    transform: translate(-50%,-50%) scale(3.5);
    opacity: 0;
  }
}
/* 2 — double ripple */

.hv-12__btn--double {
  background: var(--rose);
  color: #fff;
  overflow: visible;
}

.hv-12__btn--double::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: rgba(255,255,255,.3);
  transform: translate(-50%,-50%) scale(0);
  pointer-events: none;
}

.hv-12__btn--double::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: rgba(255,255,255,.2);
  transform: translate(-50%,-50%) scale(0);
  pointer-events: none;
}

.hv-12__btn--double:hover::before {
  animation: hv-12-ripple .7s ease-out forwards;
}

.hv-12__btn--double:hover::after {
  animation: hv-12-ripple .7s ease-out .25s forwards;
}
/* 3 — contained wave */

.hv-12__btn--wave {
  background: transparent;
  border: 2px solid var(--magenta);
  color: var(--magenta);
  overflow: hidden;
}

.hv-12__btn--wave::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  border-radius: 50%;
  background: rgba(219,39,119,.25);
  transform: translate(-50%,-50%);
  pointer-events: none;
}

.hv-12__btn--wave:hover::before {
  animation: hv-12-wave .6s ease-out forwards;
}

@keyframes hv-12-wave {
  0% {
    width: 0;
    height: 0;
    opacity: .8;
  }

  100% {
    width: 300px;
    height: 300px;
    opacity: 0;
  }
}
/* 4 — border pulse */

.hv-12__btn--pulse {
  background: var(--purple);
  color: #fff;
  outline: 2px solid var(--purple);
  outline-offset: 0;
  transition: outline-offset .4s,outline-color .4s;
}

.hv-12__btn--pulse:hover {
  outline-offset: 10px;
  outline-color: transparent;
}
/* 5 — glow ring */

.hv-12__btn--glow {
  background: transparent;
  border: 2px solid var(--pink);
  color: var(--pink);
  box-shadow: 0 0 0 0 rgba(236,72,153,.4);
  transition: box-shadow .4s cubic-bezier(.4,0,.2,1),color .3s,background .3s;
}

.hv-12__btn--glow:hover {
  box-shadow: 0 0 0 12px rgba(236,72,153,0);
  color: #fff;
  background: var(--pink);
}

@media (max-width: 500px) {
  .hv-12__row {
    flex-direction: column;
    align-items: flex-start;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hv-12__btn::before,
    .hv-12__btn::after {
    animation: none!important;
  }

  .hv-12__btn--pulse {
    transition: 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 Hover 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 Ripple Button Hover Effect
Source: https://codefronts.com/motion/css-hover-effects/css-ripple-button-hover-effect/

Five ripple and pulse hover effects on buttons — center-out ring, double ripple, contained wave, border-pulse, and glow-ring expansion — all using pseudo-element scale animations and opacity keyframes to simulate a water-droplet impact.
## HTML
```html
<div class="hv-12">
  <div class="hv-12__stack">
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--ring">Center Ripple</button>
      <span class="hv-12__label">scale + opacity keyframe</span>
    </div>
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--double">Double Ring</button>
      <span class="hv-12__label">staggered dual ripple</span>
    </div>
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--wave">Contained Wave</button>
      <span class="hv-12__label">overflow-clipped wave</span>
    </div>
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--pulse">Border Pulse</button>
      <span class="hv-12__label">outline-offset ping</span>
    </div>
    <div class="hv-12__row">
      <button class="hv-12__btn hv-12__btn--glow">Glow Ring</button>
      <span class="hv-12__label">box-shadow expand</span>
    </div>
  </div>
</div>
```
## CSS
```css
.hv-12,
.hv-12 *,
.hv-12 *::before,
.hv-12 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.hv-12 ::selection {
  background: #be185d;
  color: #fff;
}

.hv-12 {
  --bg: #0f0008;
  --text: #fce7f3;
  --dim: #6b7280;
  --pink: #ec4899;
  --rose: #f43f5e;
  --magenta: #db2777;
  --purple: #a855f7;
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: var(--bg);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 60px 24px;
}

.hv-12__stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: min(640px,100%);
}

.hv-12__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 24px 36px;
  background: rgba(255,255,255,.025);
  border: 1px solid rgba(255,255,255,.07);
  border-radius: 12px;
}

.hv-12__label {
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
  white-space: nowrap;
  flex-shrink: 0;
}
/* shared btn */

.hv-12__btn {
  position: relative;
  padding: 13px 36px;
  font-size: .95rem;
  font-weight: 600;
  letter-spacing: .05em;
  cursor: pointer;
  border-radius: 8px;
  border: none;
  flex-shrink: 0;
}
/* 1 — center ripple */

.hv-12__btn--ring {
  background: var(--pink);
  color: #fff;
  overflow: visible;
}

.hv-12__btn--ring::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: rgba(255,255,255,.4);
  transform: translate(-50%,-50%) scale(0);
  pointer-events: none;
}

.hv-12__btn--ring:hover::after {
  animation: hv-12-ripple .6s ease-out forwards;
}

@keyframes hv-12-ripple {
  0% {
    transform: translate(-50%,-50%) scale(0);
    opacity: .7;
  }

  100% {
    transform: translate(-50%,-50%) scale(3.5);
    opacity: 0;
  }
}
/* 2 — double ripple */

.hv-12__btn--double {
  background: var(--rose);
  color: #fff;
  overflow: visible;
}

.hv-12__btn--double::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: rgba(255,255,255,.3);
  transform: translate(-50%,-50%) scale(0);
  pointer-events: none;
}

.hv-12__btn--double::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  background: rgba(255,255,255,.2);
  transform: translate(-50%,-50%) scale(0);
  pointer-events: none;
}

.hv-12__btn--double:hover::before {
  animation: hv-12-ripple .7s ease-out forwards;
}

.hv-12__btn--double:hover::after {
  animation: hv-12-ripple .7s ease-out .25s forwards;
}
/* 3 — contained wave */

.hv-12__btn--wave {
  background: transparent;
  border: 2px solid var(--magenta);
  color: var(--magenta);
  overflow: hidden;
}

.hv-12__btn--wave::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  border-radius: 50%;
  background: rgba(219,39,119,.25);
  transform: translate(-50%,-50%);
  pointer-events: none;
}

.hv-12__btn--wave:hover::before {
  animation: hv-12-wave .6s ease-out forwards;
}

@keyframes hv-12-wave {
  0% {
    width: 0;
    height: 0;
    opacity: .8;
  }

  100% {
    width: 300px;
    height: 300px;
    opacity: 0;
  }
}
/* 4 — border pulse */

.hv-12__btn--pulse {
  background: var(--purple);
  color: #fff;
  outline: 2px solid var(--purple);
  outline-offset: 0;
  transition: outline-offset .4s,outline-color .4s;
}

.hv-12__btn--pulse:hover {
  outline-offset: 10px;
  outline-color: transparent;
}
/* 5 — glow ring */

.hv-12__btn--glow {
  background: transparent;
  border: 2px solid var(--pink);
  color: var(--pink);
  box-shadow: 0 0 0 0 rgba(236,72,153,.4);
  transition: box-shadow .4s cubic-bezier(.4,0,.2,1),color .3s,background .3s;
}

.hv-12__btn--glow:hover {
  box-shadow: 0 0 0 12px rgba(236,72,153,0);
  color: #fff;
  background: var(--pink);
}

@media (max-width: 500px) {
  .hv-12__row {
    flex-direction: column;
    align-items: flex-start;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hv-12__btn::before,
    .hv-12__btn::after {
    animation: none!important;
  }

  .hv-12__btn--pulse {
    transition: none!important;
  }
}
```

How this works

The classic ripple places an ::after pseudo-element as a circle centered on the button using position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%) scale(0). On hover it scales from 0 to 3–4× the button diameter using transform: scale(4) while fading from opaque to transparent via opacity: 0 — the combination creates the expanding ring effect. A keyframe drives both properties with a single animation call.

The double-ripple variant runs two @keyframes — one on ::before and one on ::after with a .3s delay offset — so a second ring chases the first. The border-pulse variant animates outline from a tight offset (outline-offset: 0) to a wide one (outline-offset: 12px) while fading the outline opacity, creating an outward-expanding halo that feels like a sonar ping.

Make it yours

  • Change ripple color by editing background: rgba(255,255,255,.5) on the pseudo-element — dark ripples work on light buttons; try rgba(0,0,0,.2).
  • Slow the ripple to .8s for a calmer, more meditative interaction — faster (.3s) reads as high-energy and reactive.
  • Make the ring thinner by using border: 2px solid rgba(255,255,255,.6) instead of a filled background on the pseudo-element for an outline-only pulse.
  • Trigger the ripple on :focus-visible as well as hover to provide clear keyboard focus feedback using the same visual language.
  • Combine with a transform: scale(1.04) on the button itself so the element also lifts slightly as the ripple expands outward.

Gotchas — read before shipping

  • The ripple pseudo-element must be a circle — set border-radius: 50% and equal width/height values, not percentage dimensions that react to the button's aspect ratio.
  • Scaling a large pseudo-element can cause paint-layer promotion — keep will-change off by default and only set it while the animation is running via a class toggle if needed.
  • On Firefox, outline-offset transitions are not always smooth — the border-pulse variant may jump in Firefox; fall back to a box-shadow approach for consistency.

Browser support

ChromeSafariFirefoxEdge
60+12+60+60+

Scale and opacity animations are compositor-only and universally supported. outline-offset animation has minor Firefox quirks.

Techniques used in this demo

Search CodeFronts

Loading…