20 CSS Animated Buttons20 / 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.
Published
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');
});
});
})();Here's a working CSS Animated Button 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 Ghost Button Background Reveal
Source: https://codefronts.com/motion/css-animated-buttons/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.
## HTML
```html
<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>
```
## CSS
```css
.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;
}
}
```
## JavaScript
```js
(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');
});
});
})();
```Here's a working CSS Animated Button 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 Ghost Button Background Reveal
Source: https://codefronts.com/motion/css-animated-buttons/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.
## HTML
```html
<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>
```
## CSS
```css
.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;
}
}
```
## JavaScript
```js
(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.
Make it yours
- 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.
Gotchas — read before shipping
- 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.