Directional Button Hover

On mouseenter and mouseleave, calculates the cursor position relative to the button and positions a circular fill element at exactly that point before scaling it to cover the button. The circle's width grows based on horizontal distance from center, making the fill wider when entering from the edges.

cssjavascripthoverbuttondirectionalanimation

Code

index.html
html
<div class="btn-wrap">
  <a href="#" data-theme="dark" data-btn-hover="" class="btn w-inline-block">
    <div class="btn__bg"></div>
    <div class="btn__circle-wrap">
      <div class="btn__circle">
        <div class="before__100"></div>
      </div>
    </div>
    <div class="btn__text">
      <p class="btn-text-p">Hover these</p>
    </div>
  </a>
  <a data-theme="light" data-btn-hover="" href="#" class="btn w-inline-block">
    <div class="btn__bg"></div>
    <div class="btn__circle-wrap">
      <div class="btn__circle">
        <div class="before__100"></div>
      </div>
    </div>
    <div class="btn__text">
      <p class="btn-text-p">Directional</p>
    </div>
  </a>
  <a data-theme="primary" data-btn-hover="" href="#" class="btn w-inline-block">
    <div class="btn__bg"></div>
    <div class="btn__circle-wrap">
      <div class="btn__circle">
        <div class="before__100"></div>
      </div>
    </div>
    <div class="btn__image"><img width="72" loading="eager" alt="Photo of Founder" src="" class="img__founder"></div>
    <div class="btn__text">
      <p class="btn-text-p">Buttons</p>
    </div>
  </a>
</div>
styles.css
css
.btn {
  cursor: pointer;
  border-radius: 1em;
  border-radius: calc(var(--btn-height) * .5);
  grid-template-rows: auto auto;
  grid-template-columns: 1fr 1fr;
  grid-auto-columns: 1fr;
  justify-content: center;
  align-items: center;
  height: 3em;
  padding-left: 1.25em;
  padding-right: 1.25em;
  text-decoration: none;
  display: flex;
  position: relative;
}

.btn__text {
  color: #efede3;
  justify-content: flex-start;
  align-items: center;
  display: flex;
  position: relative;
  transition: color 0.7s cubic-bezier(0.625, 0.05, 0, 1);
}

.btn-text-p {
  color: currentColor;
  white-space: nowrap;
  margin-bottom: 0;
  padding-bottom: .05em;
  font-size: 1em;
  font-weight: 500;
  line-height: 1.2;
}

.btn__bg {
  background-color: #08181b;
  border-radius: 1.5em;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

.btn__image {
  border-radius: 50%;
  width: 2.25em;
  height: 2.25em;
  margin-left: -.75em;
  margin-right: .5em;
  position: relative;
  overflow: hidden;
}

.img__founder {
  object-fit: cover;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

.btn__circle-wrap {
  border-radius: 1.5em;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
}

.btn__circle {
  pointer-events: none;
  background-color: #d1fd88;
  border-radius: 50%;
  width: 100%;
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  transition: transform 0.7s cubic-bezier(0.625, 0.05, 0, 1), background-color 0.4s cubic-bezier(0.625, 0.05, 0, 1);
  transform: translate(-50%, -50%) scale(0) rotate(0.001deg);
}

.before__100 {
  padding-top: 100%;
  display: block;
}

.btn .btn__text {
  transition: color 0.7s cubic-bezier(0.625, 0.05, 0, 1);
}

.btn:hover .btn__circle {
  transform: translate(-50%, -50%) scale(1) rotate(0.001deg);
}

/* Dark */
.btn[data-theme="dark"] .btn__circle {
  background-color: #D1FD88;
}

.btn[data-theme="dark"]:hover .btn__text {
  color: #031819;
}

/* Light */
.btn[data-theme="light"] .btn__bg {
  background-color: #EFEDE3;
}

.btn[data-theme="light"] .btn__text {
  color: #031819;
}

.btn[data-theme="light"] .btn__circle {
  background-color: #9FCCC8;
}

/* Primary */
.btn[data-theme="primary"] .btn__bg {
  background-color: #D1FD88;
}

.btn[data-theme="primary"] .btn__text {
  color: #031819;
}

.btn[data-theme="primary"] .btn__circle {
  background-color: #b8ec6f;
}
script.js
javascript
function initDirectionalButtonHover() {
  document.querySelectorAll('[data-btn-hover]').forEach(button => {
    button.addEventListener('mouseenter', handleHover);
    button.addEventListener('mouseleave', handleHover);
  });

  function handleHover(event) {
    const button = event.currentTarget;
    const buttonRect = button.getBoundingClientRect();

    // Get the button's dimensions and center
    const buttonWidth = buttonRect.width;
    const buttonHeight = buttonRect.height;
    const buttonCenterX = buttonRect.left + buttonWidth / 2;

    // Calculate mouse position
    const mouseX = event.clientX;
    const mouseY = event.clientY;

    // Offset from the top-left corner in percentage
    const offsetXFromLeft = ((mouseX - buttonRect.left) / buttonWidth) * 100;
    const offsetYFromTop = ((mouseY - buttonRect.top) / buttonHeight) * 100;

    // Offset from the center in percentage (absolute value)
    let offsetXFromCenter = Math.abs(((mouseX - buttonCenterX) / (buttonWidth / 2)) * 50);

    // Update position and size of .btn__circle
    const circle = button.querySelector('.btn__circle');
    if (circle) {
      circle.style.left = `${offsetXFromLeft.toFixed(1)}%`;
      circle.style.top = `${offsetYFromTop.toFixed(1)}%`;
      circle.style.width = `${115 + offsetXFromCenter.toFixed(1) * 2}%`;
    }
  }
}

// Initialize Directional Button Hover
document.addEventListener('DOMContentLoaded', function() {
  initDirectionalButtonHover();
});

Guide

How It Works

Both mouseenter and mouseleave fire handleHover. On each event the cursor position is converted to a percentage of the button's width and height, and the .btn__circle is repositioned to that exact point. CSS then scales it from 0 to 1 with a smooth transition.

Circle Sizing

The circle's width starts at 115% and grows by 2x the absolute horizontal offset from center. Entering from the far left or right makes the circle wider so it still fully covers the button despite the off-center origin point.

data-btn-hover

Add data-btn-hover to any button element to register it. The script queries all matching elements on DOMContentLoaded, so multiple buttons on the same page are all handled automatically.

Themes

Use data-theme="dark", data-theme="light", or data-theme="primary" on individual buttons to swap background and circle colours via CSS attribute selectors. Add new theme variants by following the same pattern.

Image Variant

Add a .btn__image element with an img inside before .btn__text to include an avatar or icon inside the button. The negative left margin pulls it visually into the button padding for a pill-with-avatar look.