20 CSS Animated Buttons 20 / 20
CSS Ghost Button Background Reveal
Transparent ghost buttons that elegantly fill with a solid colour or gradient on hover, with JS-powered ripple origin tracking so the fill emanates from the exact cursor position.
The code
<div class="ab-20">
<p class="ab-20__label">Hover from any direction</p>
<div class="ab-20__row">
<button class="ab-20__btn ab-20__btn--violet">
<span class="ab-20__fill"></span>
<span class="ab-20__label-inner">Get Started</span>
</button>
<button class="ab-20__btn ab-20__btn--cyan">
<span class="ab-20__fill"></span>
<span class="ab-20__label-inner">View Docs</span>
</button>
<button class="ab-20__btn ab-20__btn--rose">
<span class="ab-20__fill"></span>
<span class="ab-20__label-inner">Contact Us</span>
</button>
</div>
</div> <div class="ab-20">
<p class="ab-20__label">Hover from any direction</p>
<div class="ab-20__row">
<button class="ab-20__btn ab-20__btn--violet">
<span class="ab-20__fill"></span>
<span class="ab-20__label-inner">Get Started</span>
</button>
<button class="ab-20__btn ab-20__btn--cyan">
<span class="ab-20__fill"></span>
<span class="ab-20__label-inner">View Docs</span>
</button>
<button class="ab-20__btn ab-20__btn--rose">
<span class="ab-20__fill"></span>
<span class="ab-20__label-inner">Contact Us</span>
</button>
</div>
</div>.ab-20,.ab-20 *,.ab-20 *::before,.ab-20 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-20 ::selection{background:#7c3aed;color:#fff}
.ab-20{
font-family:system-ui,sans-serif;
background:#0a0a14;
min-height:100vh;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:2rem;
padding:2rem;
}
.ab-20__label{font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#475569}
.ab-20__row{display:flex;gap:1rem;flex-wrap:wrap;justify-content:center}
.ab-20__btn{
--fill-color:#7c3aed;
position:relative;
overflow:hidden;
padding:.85rem 2.2rem;
font-size:.95rem;
font-weight:700;
color:#e2e8f0;
background:transparent;
border:2px solid rgba(255,255,255,.2);
border-radius:10px;
cursor:pointer;
outline:none;
transition:color .35s,border-color .35s;
}
.ab-20__btn--violet{--fill-color:#7c3aed;color:#c4b5fd;border-color:#7c3aed}
.ab-20__btn--cyan{--fill-color:#0891b2;color:#67e8f9;border-color:#0891b2}
.ab-20__btn--rose{--fill-color:#e11d48;color:#fda4af;border-color:#e11d48}
.ab-20__btn.is-hovered{color:#fff;border-color:transparent}
.ab-20__fill{
position:absolute;
border-radius:50%;
background:var(--fill-color);
left:var(--rx,50%);
top:var(--ry,50%);
width:0;height:0;
transform:translate(-50%,-50%);
transition:width .5s cubic-bezier(.4,0,.2,1),height .5s cubic-bezier(.4,0,.2,1);
pointer-events:none;
z-index:0;
}
.ab-20__btn.is-hovered .ab-20__fill{
width:var(--size,400px);
height:var(--size,400px);
}
.ab-20__label-inner{position:relative;z-index:1}
@media(prefers-reduced-motion:reduce){
.ab-20__fill{transition:none}
.ab-20__btn.is-hovered .ab-20__fill{width:100%;height:100%;border-radius:0}
} .ab-20,.ab-20 *,.ab-20 *::before,.ab-20 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-20 ::selection{background:#7c3aed;color:#fff}
.ab-20{
font-family:system-ui,sans-serif;
background:#0a0a14;
min-height:100vh;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:2rem;
padding:2rem;
}
.ab-20__label{font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#475569}
.ab-20__row{display:flex;gap:1rem;flex-wrap:wrap;justify-content:center}
.ab-20__btn{
--fill-color:#7c3aed;
position:relative;
overflow:hidden;
padding:.85rem 2.2rem;
font-size:.95rem;
font-weight:700;
color:#e2e8f0;
background:transparent;
border:2px solid rgba(255,255,255,.2);
border-radius:10px;
cursor:pointer;
outline:none;
transition:color .35s,border-color .35s;
}
.ab-20__btn--violet{--fill-color:#7c3aed;color:#c4b5fd;border-color:#7c3aed}
.ab-20__btn--cyan{--fill-color:#0891b2;color:#67e8f9;border-color:#0891b2}
.ab-20__btn--rose{--fill-color:#e11d48;color:#fda4af;border-color:#e11d48}
.ab-20__btn.is-hovered{color:#fff;border-color:transparent}
.ab-20__fill{
position:absolute;
border-radius:50%;
background:var(--fill-color);
left:var(--rx,50%);
top:var(--ry,50%);
width:0;height:0;
transform:translate(-50%,-50%);
transition:width .5s cubic-bezier(.4,0,.2,1),height .5s cubic-bezier(.4,0,.2,1);
pointer-events:none;
z-index:0;
}
.ab-20__btn.is-hovered .ab-20__fill{
width:var(--size,400px);
height:var(--size,400px);
}
.ab-20__label-inner{position:relative;z-index:1}
@media(prefers-reduced-motion:reduce){
.ab-20__fill{transition:none}
.ab-20__btn.is-hovered .ab-20__fill{width:100%;height:100%;border-radius:0}
}(function(){
var btns=document.querySelectorAll('.ab-20__btn');
btns.forEach(function(btn){
var fill=btn.querySelector('.ab-20__fill');
if(!fill)return;
var diag=Math.sqrt(Math.pow(btn.offsetWidth,2)+Math.pow(btn.offsetHeight,2));
btn.style.setProperty('--size',diag*3+'px');
btn.addEventListener('mouseenter',function(e){
fill.style.setProperty('--rx',e.offsetX+'px');
fill.style.setProperty('--ry',e.offsetY+'px');
btn.classList.add('is-hovered');
});
btn.addEventListener('mouseleave',function(e){
fill.style.setProperty('--rx',e.offsetX+'px');
fill.style.setProperty('--ry',e.offsetY+'px');
btn.classList.remove('is-hovered');
});
});
})(); (function(){
var btns=document.querySelectorAll('.ab-20__btn');
btns.forEach(function(btn){
var fill=btn.querySelector('.ab-20__fill');
if(!fill)return;
var diag=Math.sqrt(Math.pow(btn.offsetWidth,2)+Math.pow(btn.offsetHeight,2));
btn.style.setProperty('--size',diag*3+'px');
btn.addEventListener('mouseenter',function(e){
fill.style.setProperty('--rx',e.offsetX+'px');
fill.style.setProperty('--ry',e.offsetY+'px');
btn.classList.add('is-hovered');
});
btn.addEventListener('mouseleave',function(e){
fill.style.setProperty('--rx',e.offsetX+'px');
fill.style.setProperty('--ry',e.offsetY+'px');
btn.classList.remove('is-hovered');
});
});
})();How this works
In the resting state each button has a transparent background and a visible border — the classic ghost button look. The fill lives in an absolutely-positioned span.ab-20__fill inside each button. This span starts as a tiny circle (width: 0; height: 0; border-radius: 50%) at the cursor's entry position, computed in JS from event.offsetX/Y and set via CSS custom properties --rx/--ry. On hover, a CSS transition scales the fill circle to width: var(--size); height: var(--size) where --size is set to 300% of the button's diagonal length, large enough to cover the entire button area regardless of entry point.
On mouse-leave, JS reads the exit position and sets new --rx/--ry coordinates before removing the .is-hovered class, making the fill collapse back toward the exit point rather than an arbitrary corner. This bidirectional, cursor-aware animation is impossible with CSS-only :hover and is the key reason this demo uses JS. All rendering is done via CSS transitions — JS only sets coordinates and toggles the class.
Customize
- Change the fill colour per button by setting a
data-fillattribute and reading it in JS to apply as abackgroundon the fill span. - Use a gradient fill by setting the fill span's
backgroundtolinear-gradient(135deg, #7c3aed, #2563eb)for a premium reveal effect. - Adjust the expand speed by changing the
transition-durationon.ab-20__fillfrom.5sto.3sfor a snappier expand. - Cap the fill radius for a partial-reveal aesthetic by reducing
--sizein JS to150%of the diagonal instead of300%. - Combine with the border-draw technique (demo 04) by removing the button border initially and only showing it after the fill has contracted back out.
Watch out for
- The fill span must have
pointer-events: noneto prevent it from capturing themouseleaveevent as the cursor crosses onto the growing fill layer — without this the exit animation fires prematurely. - Setting
overflow: hiddenon the button is essential to clip the expanding circle — also ensure the button hasposition: relativeso the fill span's absolute positioning is correctly anchored. - The cursor-origin approach uses
event.offsetX/Ywhich is relative to the element's padding edge — if the button has significant padding, the origin will appear slightly inset from the true cursor position; usegetBoundingClientRect()for pixel-perfect accuracy.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 49+ | 10.1+ | 44+ | 49+ |
Uses standard DOM events and CSS transitions; supported in all modern browsers.