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.
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> <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}
} .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
--liquidon the button variant — gradients work too:linear-gradient(180deg, #06b6d4, #0284c7). - Adjust the rise speed by editing the
transition-durationon::beforefrom.6sto1sfor a slow, dramatic fill. - Tune the wave shape by editing the
border-radiuspercentages on::before— more asymmetric values create a more pronounced wave crest. - Add a foam texture by overlaying a second semi-transparent
::afterpseudo-element with a lighter shade and a slight animation-delay offset. - Pair with a fill-percentage indicator by JS-controlling the initial
translateYvalue to partially fill the button as a progress metaphor.
Watch out for
- The
overflow: hiddenrequired to clip the pseudo-element also clips any outerbox-shadow— use a wrappingdivwith a drop-shadow filter if you need a glow around the button. - The
border-radiusanimation on::beforecauses paint but is contained to the pseudo-element layer — avoid placing many of these buttons in rapidly scrolling lists. - On Firefox, very high
border-radiuspercentage values (> 60%) on a pseudo-element can visually collapse the element if width/height are not explicitly set.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 49+ | 10.1+ | 54+ | 49+ |
Uses standard CSS transforms and border-radius animations; broadly supported.