20 CSS Animated Buttons 06 / 20

CSS Liquid Fill Button Animation

A button whose interior fills with a rising liquid wave on hover, created entirely with a CSS pseudo-element, SVG-like border-radius shaping, and a translateY animation.

Pure CSS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="ab-06">
  <p class="ab-06__label">Hover to fill</p>
  <div class="ab-06__row">
    <button class="ab-06__btn ab-06__btn--cyan"><span>Explore</span></button>
    <button class="ab-06__btn ab-06__btn--violet"><span>Subscribe</span></button>
    <button class="ab-06__btn ab-06__btn--amber"><span>Upgrade</span></button>
  </div>
</div>
.ab-06,.ab-06 *,.ab-06 *::before,.ab-06 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-06 ::selection{background:#0891b2;color:#fff}
.ab-06{
  font-family:system-ui,sans-serif;
  background:#0f172a;
  min-height:100vh;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:2.5rem;
  padding:2rem;
}
.ab-06__label{
  font-size:.78rem;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:#94a3b8;
  opacity:.6;
}
.ab-06__row{
  display:flex;
  gap:1.25rem;
  flex-wrap:wrap;
  justify-content:center;
}
.ab-06__btn{
  --liquid:#0891b2;
  --border-col:#0891b2;
  position:relative;
  overflow:hidden;
  padding:.85rem 2.2rem;
  font-size:1rem;
  font-weight:700;
  color:#e2e8f0;
  background:transparent;
  border:2px solid var(--border-col);
  border-radius:10px;
  cursor:pointer;
  outline:none;
  letter-spacing:.03em;
  transition:color .4s;
}
.ab-06__btn--cyan{--liquid:#0891b2;--border-col:#22d3ee;color:#67e8f9}
.ab-06__btn--violet{--liquid:#7c3aed;--border-col:#a78bfa;color:#c4b5fd}
.ab-06__btn--amber{--liquid:#d97706;--border-col:#fbbf24;color:#fde68a}
.ab-06__btn span{position:relative;z-index:1}
.ab-06__btn::before{
  content:'';
  position:absolute;
  bottom:0;left:-25%;
  width:150%;height:200%;
  background:var(--liquid);
  border-radius:45% 55% 0 0 / 25% 25% 0 0;
  transform:translateY(100%);
  transition:transform .65s cubic-bezier(.4,0,.2,1);
  z-index:0;
}
.ab-06__btn:hover::before{
  transform:translateY(5%);
  animation:ab-06-wave 2.5s ease-in-out infinite .65s;
}
.ab-06__btn:hover{color:#fff}
@keyframes ab-06-wave{
  0%,100%{border-radius:45% 55% 0 0 / 25% 25% 0 0}
  50%{border-radius:55% 45% 0 0 / 20% 30% 0 0}
}
@media(prefers-reduced-motion:reduce){
  .ab-06__btn::before{transition:none}
  .ab-06__btn:hover::before{animation:none}
}

How this works

The liquid fill lives in the ::before pseudo-element, sized 200% of the button width and 150% of the height so it extends well beyond the button edges. In its resting state it is translated down 100%, hiding it below the button (clipped by overflow: hidden). On :hover, a transition: transform .6s cubic-bezier(.4,0,.2,1) slides it upward to translateY(-5%), simulating a liquid rising to fill the space. The wave shape on the top edge comes from an asymmetric border-radius such as 45% 55% 0 0 / 20% 20% 0 0 — a gentle undulation that reads as a fluid surface.

A secondary ripple keyframe (ab-06-wave) gently oscillates the border-radius values on the pseudo-element while hovered, making the surface of the "liquid" appear alive and in motion. The button text colour transitions from the accent to white as the fill rises beneath it, timed to roughly match when the fill crosses the midpoint of the label. All animated properties are transform and border-radius — only border-radius causes minor paint updates but they are brief and isolated to the small pseudo-element.

Customize

  • Change the liquid colour by updating --liquid on the button variant — gradients work too: linear-gradient(180deg, #06b6d4, #0284c7).
  • Adjust the rise speed by editing the transition-duration on ::before from .6s to 1s for a slow, dramatic fill.
  • Tune the wave shape by editing the border-radius percentages on ::before — more asymmetric values create a more pronounced wave crest.
  • Add a foam texture by overlaying a second semi-transparent ::after pseudo-element with a lighter shade and a slight animation-delay offset.
  • Pair with a fill-percentage indicator by JS-controlling the initial translateY value to partially fill the button as a progress metaphor.

Watch out for

  • The overflow: hidden required to clip the pseudo-element also clips any outer box-shadow — use a wrapping div with a drop-shadow filter if you need a glow around the button.
  • The border-radius animation on ::before causes paint but is contained to the pseudo-element layer — avoid placing many of these buttons in rapidly scrolling lists.
  • On Firefox, very high border-radius percentage values (> 60%) on a pseudo-element can visually collapse the element if width/height are not explicitly set.

Browser support

ChromeSafariFirefoxEdge
49+ 10.1+ 54+ 49+

Uses standard CSS transforms and border-radius animations; broadly supported.

Search CodeFronts

Loading…