20 CSS Animated Buttons20 / 20

CSS + JSMIT licensed

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

Live Demo
Try it

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>
.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');
    });
  });
})();
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 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-fill attribute and reading it in JS to apply as a background on the fill span.
  • Use a gradient fill by setting the fill span's background to linear-gradient(135deg, #7c3aed, #2563eb) for a premium reveal effect.
  • Adjust the expand speed by changing the transition-duration on .ab-20__fill from .5s to .3s for a snappier expand.
  • Cap the fill radius for a partial-reveal aesthetic by reducing --size in JS to 150% of the diagonal instead of 300%.
  • 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: none to prevent it from capturing the mouseleave event as the cursor crosses onto the growing fill layer — without this the exit animation fires prematurely.
  • Setting overflow: hidden on the button is essential to clip the expanding circle — also ensure the button has position: relative so the fill span's absolute positioning is correctly anchored.
  • The cursor-origin approach uses event.offsetX/Y which 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; use getBoundingClientRect() for pixel-perfect accuracy.

Browser support

ChromeSafariFirefoxEdge
49+10.1+44+49+

Uses standard DOM events and CSS transitions; supported in all modern browsers.

Techniques used in this demo

Search CodeFronts

Loading…