Momentum Based Hover (Inertia)

GSAP InertiaPlugin-powered hover effect that reads pointer velocity as it enters an element and launches the card into a physics-based x, y, and rotation tween that decelerates naturally. Works on any existing markup — just apply three data attributes.

gsapinertiamomentumhoverphysicsvelocityrotation

Setup — External Scripts

CDN — GSAP (add before </body>)
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
CDN — InertiaPlugin (add after GSAP)
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/InertiaPlugin.min.js"></script>

Code

HTML
html
<section data-momentum-hover-init="" class="section-resource">
  <div class="section-resource__collection">
    <div class="section-resource__list">
      <!-- Card 1 -->
      <div class="section-resource__item">
        <div data-momentum-hover-element="" class="demo-card__wrap">
          <div data-momentum-hover-target="" class="demo-card">
            <div class="demo-card__before"></div>
            <img src="https://cdn.prod.website-files.com/684d7179e99d2a34dfe75d9f/686b8c064494dac669a1b7ed_portrait-1.jpg" loading="eager" alt="Jess, Co-Founder" class="demo-card__image">
            <div class="demo-card__content">
              <div class="demo-card__name">
                <h3 class="demo-card__h3">Jess</h3>
                <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 24 24" fill="none" class="demo-card__check-svg"><path d="M13.06 3.06005L14.56 4.56005C14.8411 4.84143 15.2224 4.9997 15.62 5.00005H17.5C17.8979 5.00005 18.2794 5.15808 18.5607 5.43939C18.842 5.72069 19 6.10222 19 6.50005V8.38005C19.0004 8.77774 19.1587 9.15901 19.44 9.44005L20.94 10.94C21.0803 11.0786 21.1917 11.2437 21.2677 11.4257C21.3438 11.6076 21.3829 11.8028 21.3829 12C21.3829 12.1972 21.3438 12.3925 21.2677 12.5744C21.1917 12.7564 21.0803 12.9215 20.94 13.06L19.44 14.56C19.1587 14.8411 19.0004 15.2224 19 15.62V17.5C19 17.8979 18.842 18.2794 18.5607 18.5607C18.2794 18.842 17.8979 19 17.5 19H15.62C15.2224 19.0004 14.8411 19.1587 14.56 19.44L13.06 20.94C12.9215 21.0803 12.7564 21.1917 12.5744 21.2677C12.3925 21.3438 12.1972 21.3829 12 21.3829C11.8028 21.3829 11.6076 21.3438 11.4257 21.2677C11.2437 21.1917 11.0786 21.0803 10.94 20.94L9.44005 19.44C9.15901 19.1587 8.77774 19.0004 8.38005 19H6.50005C6.10222 19 5.72069 18.842 5.43939 18.5607C5.15808 18.2794 5.00005 17.8979 5.00005 17.5V15.62C4.9997 15.2224 4.84143 14.8411 4.56005 14.56L3.06005 13.06C2.91976 12.9215 2.80837 12.7564 2.73235 12.5744C2.65633 12.3925 2.61719 12.1972 2.61719 12C2.61719 11.8028 2.65633 11.6076 2.73235 11.4257C2.80837 11.2437 2.91976 11.0786 3.06005 10.94L4.56005 9.44005C4.84143 9.15901 4.9997 8.77774 5.00005 8.38005V6.50005C5.00005 6.10222 5.15808 5.72069 5.43939 5.43939C5.72069 5.15808 6.10222 5.00005 6.50005 5.00005H8.38005C8.77774 4.9997 9.15901 4.84143 9.44005 4.56005L10.94 3.06005C11.0786 2.91976 11.2437 2.80837 11.4257 2.73235C11.6076 2.65633 11.8028 2.61719 12 2.61719C12.1972 2.61719 12.3925 2.65633 12.5744 2.73235C12.7564 2.80837 12.9215 2.91976 13.06 3.06005Z" fill="#D3FF5B"></path><path d="M8.5 11.5L11 14L15 10" stroke="black" stroke-width="1.5" stroke-miterlimit="10"></path></svg>
              </div>
              <p class="demo-card__job-title-p">Co-Founder</p>
            </div>
          </div>
        </div>
      </div>
      <!-- Card 2 -->
      <div class="section-resource__item">
        <div data-momentum-hover-element="" class="demo-card__wrap">
          <div data-momentum-hover-target="" class="demo-card">
            <div class="demo-card__before"></div>
            <img src="https://cdn.prod.website-files.com/684d7179e99d2a34dfe75d9f/686b8c06662f765388fa0e57_portrait-2.jpg" loading="eager" alt="Edward, Finance" class="demo-card__image">
            <div class="demo-card__content">
              <div class="demo-card__name">
                <h3 class="demo-card__h3">Edward</h3>
                <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 24 24" fill="none" class="demo-card__check-svg"><path d="M13.06 3.06005L14.56 4.56005C14.8411 4.84143 15.2224 4.9997 15.62 5.00005H17.5C17.8979 5.00005 18.2794 5.15808 18.5607 5.43939C18.842 5.72069 19 6.10222 19 6.50005V8.38005C19.0004 8.77774 19.1587 9.15901 19.44 9.44005L20.94 10.94C21.0803 11.0786 21.1917 11.2437 21.2677 11.4257C21.3438 11.6076 21.3829 11.8028 21.3829 12C21.3829 12.1972 21.3438 12.3925 21.2677 12.5744C21.1917 12.7564 21.0803 12.9215 20.94 13.06L19.44 14.56C19.1587 14.8411 19.0004 15.2224 19 15.62V17.5C19 17.8979 18.842 18.2794 18.5607 18.5607C18.2794 18.842 17.8979 19 17.5 19H15.62C15.2224 19.0004 14.8411 19.1587 14.56 19.44L13.06 20.94C12.9215 21.0803 12.7564 21.1917 12.5744 21.2677C12.3925 21.3438 12.1972 21.3829 12 21.3829C11.8028 21.3829 11.6076 21.3438 11.4257 21.2677C11.2437 21.1917 11.0786 21.0803 10.94 20.94L9.44005 19.44C9.15901 19.1587 8.77774 19.0004 8.38005 19H6.50005C6.10222 19 5.72069 18.842 5.43939 18.5607C5.15808 18.2794 5.00005 17.8979 5.00005 17.5V15.62C4.9997 15.2224 4.84143 14.8411 4.56005 14.56L3.06005 13.06C2.91976 12.9215 2.80837 12.7564 2.73235 12.5744C2.65633 12.3925 2.61719 12.1972 2.61719 12C2.61719 11.8028 2.65633 11.6076 2.73235 11.4257C2.80837 11.2437 2.91976 11.0786 3.06005 10.94L4.56005 9.44005C4.84143 9.15901 4.9997 8.77774 5.00005 8.38005V6.50005C5.00005 6.10222 5.15808 5.72069 5.43939 5.43939C5.72069 5.15808 6.10222 5.00005 6.50005 5.00005H8.38005C8.77774 4.9997 9.15901 4.84143 9.44005 4.56005L10.94 3.06005C11.0786 2.91976 11.2437 2.80837 11.4257 2.73235C11.6076 2.65633 11.8028 2.61719 12 2.61719C12.1972 2.61719 12.3925 2.65633 12.5744 2.73235C12.7564 2.80837 12.9215 2.91976 13.06 3.06005Z" fill="#D3FF5B"></path><path d="M8.5 11.5L11 14L15 10" stroke="black" stroke-width="1.5" stroke-miterlimit="10"></path></svg>
              </div>
              <p class="demo-card__job-title-p">Finance</p>
            </div>
          </div>
        </div>
      </div>
      <!-- Card 3 -->
      <div class="section-resource__item">
        <div data-momentum-hover-element="" class="demo-card__wrap">
          <div data-momentum-hover-target="" class="demo-card">
            <div class="demo-card__before"></div>
            <img src="https://cdn.prod.website-files.com/684d7179e99d2a34dfe75d9f/686b8c074f2f56abd265e05a_portrait-3.jpg" loading="eager" alt="Mel, Product Designer" class="demo-card__image">
            <div class="demo-card__content">
              <div class="demo-card__name">
                <h3 class="demo-card__h3">Mel</h3>
                <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 24 24" fill="none" class="demo-card__check-svg"><path d="M13.06 3.06005L14.56 4.56005C14.8411 4.84143 15.2224 4.9997 15.62 5.00005H17.5C17.8979 5.00005 18.2794 5.15808 18.5607 5.43939C18.842 5.72069 19 6.10222 19 6.50005V8.38005C19.0004 8.77774 19.1587 9.15901 19.44 9.44005L20.94 10.94C21.0803 11.0786 21.1917 11.2437 21.2677 11.4257C21.3438 11.6076 21.3829 11.8028 21.3829 12C21.3829 12.1972 21.3438 12.3925 21.2677 12.5744C21.1917 12.7564 21.0803 12.9215 20.94 13.06L19.44 14.56C19.1587 14.8411 19.0004 15.2224 19 15.62V17.5C19 17.8979 18.842 18.2794 18.5607 18.5607C18.2794 18.842 17.8979 19 17.5 19H15.62C15.2224 19.0004 14.8411 19.1587 14.56 19.44L13.06 20.94C12.9215 21.0803 12.7564 21.1917 12.5744 21.2677C12.3925 21.3438 12.1972 21.3829 12 21.3829C11.8028 21.3829 11.6076 21.3438 11.4257 21.2677C11.2437 21.1917 11.0786 21.0803 10.94 20.94L9.44005 19.44C9.15901 19.1587 8.77774 19.0004 8.38005 19H6.50005C6.10222 19 5.72069 18.842 5.43939 18.5607C5.15808 18.2794 5.00005 17.8979 5.00005 17.5V15.62C4.9997 15.2224 4.84143 14.8411 4.56005 14.56L3.06005 13.06C2.91976 12.9215 2.80837 12.7564 2.73235 12.5744C2.65633 12.3925 2.61719 12.1972 2.61719 12C2.61719 11.8028 2.65633 11.6076 2.73235 11.4257C2.80837 11.2437 2.91976 11.0786 3.06005 10.94L4.56005 9.44005C4.84143 9.15901 4.9997 8.77774 5.00005 8.38005V6.50005C5.00005 6.10222 5.15808 5.72069 5.43939 5.43939C5.72069 5.15808 6.10222 5.00005 6.50005 5.00005H8.38005C8.77774 4.9997 9.15901 4.84143 9.44005 4.56005L10.94 3.06005C11.0786 2.91976 11.2437 2.80837 11.4257 2.73235C11.6076 2.65633 11.8028 2.61719 12 2.61719C12.1972 2.61719 12.3925 2.65633 12.5744 2.73235C12.7564 2.80837 12.9215 2.91976 13.06 3.06005Z" fill="#D3FF5B"></path><path d="M8.5 11.5L11 14L15 10" stroke="black" stroke-width="1.5" stroke-miterlimit="10"></path></svg>
              </div>
              <p class="demo-card__job-title-p">Product Designer</p>
            </div>
          </div>
        </div>
        <div data-momentum-hover-element="" class="demo-badge__wrap">
          <div data-momentum-hover-target="" class="demo-badge"><img src="https://cdn.prod.website-files.com/684d7179e99d2a34dfe75d9f/686b8faf73f8f7c44bcab43d_badge.jpg" alt="" class="demo-badge__img"></div>
        </div>
      </div>
      <!-- Card 4 -->
      <div class="section-resource__item">
        <div data-momentum-hover-element="" class="demo-card__wrap">
          <div data-momentum-hover-target="" class="demo-card">
            <div class="demo-card__before"></div>
            <img src="https://cdn.prod.website-files.com/684d7179e99d2a34dfe75d9f/686b8c0642f1062f8a5ab064_portrait-4.jpg" loading="eager" alt="Steve, Marketing &amp; Support" class="demo-card__image">
            <div class="demo-card__content">
              <div class="demo-card__name">
                <h3 class="demo-card__h3">Steve</h3>
                <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewBox="0 0 24 24" fill="none" class="demo-card__check-svg"><path d="M13.06 3.06005L14.56 4.56005C14.8411 4.84143 15.2224 4.9997 15.62 5.00005H17.5C17.8979 5.00005 18.2794 5.15808 18.5607 5.43939C18.842 5.72069 19 6.10222 19 6.50005V8.38005C19.0004 8.77774 19.1587 9.15901 19.44 9.44005L20.94 10.94C21.0803 11.0786 21.1917 11.2437 21.2677 11.4257C21.3438 11.6076 21.3829 11.8028 21.3829 12C21.3829 12.1972 21.3438 12.3925 21.2677 12.5744C21.1917 12.7564 21.0803 12.9215 20.94 13.06L19.44 14.56C19.1587 14.8411 19.0004 15.2224 19 15.62V17.5C19 17.8979 18.842 18.2794 18.5607 18.5607C18.2794 18.842 17.8979 19 17.5 19H15.62C15.2224 19.0004 14.8411 19.1587 14.56 19.44L13.06 20.94C12.9215 21.0803 12.7564 21.1917 12.5744 21.2677C12.3925 21.3438 12.1972 21.3829 12 21.3829C11.8028 21.3829 11.6076 21.3438 11.4257 21.2677C11.2437 21.1917 11.0786 21.0803 10.94 20.94L9.44005 19.44C9.15901 19.1587 8.77774 19.0004 8.38005 19H6.50005C6.10222 19 5.72069 18.842 5.43939 18.5607C5.15808 18.2794 5.00005 17.8979 5.00005 17.5V15.62C4.9997 15.2224 4.84143 14.8411 4.56005 14.56L3.06005 13.06C2.91976 12.9215 2.80837 12.7564 2.73235 12.5744C2.65633 12.3925 2.61719 12.1972 2.61719 12C2.61719 11.8028 2.65633 11.6076 2.73235 11.4257C2.80837 11.2437 2.91976 11.0786 3.06005 10.94L4.56005 9.44005C4.84143 9.15901 4.9997 8.77774 5.00005 8.38005V6.50005C5.00005 6.10222 5.15808 5.72069 5.43939 5.43939C5.72069 5.15808 6.10222 5.00005 6.50005 5.00005H8.38005C8.77774 4.9997 9.15901 4.84143 9.44005 4.56005L10.94 3.06005C11.0786 2.91976 11.2437 2.80837 11.4257 2.73235C11.6076 2.65633 11.8028 2.61719 12 2.61719C12.1972 2.61719 12.3925 2.65633 12.5744 2.73235C12.7564 2.80837 12.9215 2.91976 13.06 3.06005Z" fill="#D3FF5B"></path><path d="M8.5 11.5L11 14L15 10" stroke="black" stroke-width="1.5" stroke-miterlimit="10"></path></svg>
              </div>
              <p class="demo-card__job-title-p">Marketing &amp; Support</p>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</section>
CSS
css
.section-resource {
  justify-content: center;
  align-items: center;
  min-height: 100svh;
  padding-top: 7.5em;
  padding-bottom: 7.5em;
  display: flex;
  background-color: #393933;
}

.section-resource__collection {
  flex: 1;
  justify-content: center;
  max-width: 80em;
  padding-left: 3em;
  padding-right: 3em;
}

.section-resource__list {
  grid-column-gap: 2em;
  grid-row-gap: 2em;
  flex-flow: wrap;
  height: 100%;
  display: flex;
}

.section-resource__item {
  width: calc(25% - 0.25px - 1.5em);
  position: relative;
}

.demo-card__wrap {
  width: 100%;
  position: relative;
}

.demo-card {
  color: #fff;
  border-radius: 1.5em;
  width: 100%;
  position: relative;
  overflow: hidden;
}

.demo-card__before {
  pointer-events: none;
  padding-top: 150%;
}

.demo-card__image {
  object-fit: cover;
  width: 100%;
  max-width: none;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

.demo-card__content {
  grid-column-gap: .5em;
  grid-row-gap: .5em;
  flex-flow: column;
  justify-content: flex-end;
  width: 100%;
  height: 100%;
  padding: 2em;
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
}

.demo-card__name {
  grid-column-gap: .25em;
  grid-row-gap: .25em;
  justify-content: flex-start;
  align-items: center;
  display: flex;
}

.demo-card__check-svg {
  width: 1.5em;
}

.demo-card__h3 {
  margin-top: 0;
  margin-bottom: .0625em;
  font-size: 1.5em;
  font-weight: 500;
  line-height: 1;
}

.demo-card__job-title-p {
  opacity: .67;
  margin-bottom: 0;
  font-size: 1em;
  font-weight: 400;
  line-height: 1;
}

.demo-badge__wrap {
  z-index: 5;
  border-radius: 50%;
  width: 16em;
  position: absolute;
  top: 25%;
  right: 0%;
  transform: translate(50%, -50%);
}

.demo-badge {
  border-radius: 50%;
  width: 50%;
  margin: 25%;
  position: relative;
  overflow: hidden;
}

.demo-badge__img {
  width: 100%;
}

@media screen and (max-width: 991px) {
  .section-resource__item {
    width: calc(50% - 0.5px - 1em);
  }
}

@media screen and (max-width: 767px) {
  .section-resource__item {
    width: 100%;
  }
}
JavaScript
javascript
gsap.registerPlugin(InertiaPlugin);

function initMomentumBasedHover() {

  // If this device can't hover with a fine pointer, stop here
  if (!window.matchMedia("(hover: hover) and (pointer: fine)").matches) { return; }

  // Configuration (tweak these for feel)
  const xyMultiplier       = 30;  // multiplies pointer velocity for x/y movement
  const rotationMultiplier = 20;  // multiplies normalized torque for rotation speed
  const inertiaResistance  = 200; // higher = stops sooner

  // Pre-build clamp functions for performance
  const clampXY  = gsap.utils.clamp(-1080, 1080);
  const clampRot = gsap.utils.clamp(-60, 60);

  // Initialize each root container
  document.querySelectorAll('[data-momentum-hover-init]').forEach(root => {
    let prevX = 0, prevY = 0;
    let velX  = 0, velY  = 0;
    let rafId = null;

    // Track pointer velocity (throttled to RAF)
    root.addEventListener('mousemove', e => {
      if (rafId) return;
      rafId = requestAnimationFrame(() => {
        velX = e.clientX - prevX;
        velY = e.clientY - prevY;
        prevX = e.clientX;
        prevY = e.clientY;
        rafId = null;
      });
    });

    // Attach hover inertia to each child element
    root.querySelectorAll('[data-momentum-hover-element]').forEach(el => {
      el.addEventListener('mouseenter', e => {
        const target = el.querySelector('[data-momentum-hover-target]');
        if (!target) return;

        // Compute offset from center to pointer
        const { left, top, width, height } = target.getBoundingClientRect();
        const centerX = left + width / 2;
        const centerY = top + height / 2;
        const offsetX = e.clientX - centerX;
        const offsetY = e.clientY - centerY;

        // Compute raw torque (px²/frame)
        const rawTorque = offsetX * velY - offsetY * velX;

        // Normalize torque so rotation ∝ pointer speed (deg/sec)
        const leverDist    = Math.hypot(offsetX, offsetY) || 1;
        const angularForce = rawTorque / leverDist;

        // Calculate and clamp velocities
        const velocityX        = clampXY(velX * xyMultiplier);
        const velocityY        = clampXY(velY * xyMultiplier);
        const rotationVelocity = clampRot(angularForce * rotationMultiplier);

        // Apply GSAP inertia tween
        gsap.to(target, {
          inertia: {
            x:        { velocity: velocityX,        end: 0 },
            y:        { velocity: velocityY,        end: 0 },
            rotation: { velocity: rotationVelocity, end: 0 },
            resistance: inertiaResistance
          }
        });
      });
    });
  });
}

// Initialize Momentum Based Hover (Inertia)
document.addEventListener("DOMContentLoaded", () => {
  initMomentumBasedHover();
});

Attributes

NameTypeDefaultDescription
[data-momentum-hover-init]attributeThe section or container element. The script attaches a mousemove listener here to track pointer velocity. Add enough padding above so velocity can build before the pointer reaches the first element.
[data-momentum-hover-element]attributeThe hover trigger wrapper. Listens for mouseenter and reads the current pointer velocity at that moment. Does not animate itself — it triggers animation on its nested [data-momentum-hover-target].
[data-momentum-hover-target]attributeThe element that physically animates. Receives an inertia tween on x, y, and rotation based on the pointer velocity and position relative to the element's center at the moment of entry.

Notes

  • Requires GSAP and InertiaPlugin loaded via CDN before the script runs.
  • The HTML and CSS are optional — the effect works on any existing markup. Simply apply the three data attributes to your own elements.
  • Velocity is measured as pixel delta per animation frame (RAF-throttled), so it accurately reflects how fast the pointer was moving at the moment of entry.
  • Torque (rotation) is calculated using the cross product of the offset vector and the velocity vector, then normalized by the lever distance so rotation scales with pointer speed rather than position.
  • gsap.utils.clamp() is pre-built outside the event loop so it is not recreated on every mouseenter.
  • The effect auto-disables on touch and non-fine-pointer devices via the (hover: hover) and (pointer: fine) media query check.
  • Multiple [data-momentum-hover-init] containers on the same page are fully supported — each tracks its own velocity independently.

Guide

Momentum Container

Add the [data-momentum-hover-init] attribute to the section that you would like to have elements with the hover effect. The script will look inside for elements to animate and track your mouse speed and position. Make sure to add some extra padding above so the script can properly track your mouse before it enters the element.

Hover Element (wrapper)

Add the [data-momentum-hover-element] attribute to the element that should respond to the hover interaction. This element listens for the pointer entering, but it won't animate itself. It triggers the momentum animation on the nested target element instead.

Animated Target

Add the [data-momentum-hover-target] attribute to the element you want to animate with inertia. This is the part that visually moves when the user hovers — it will respond with x, y, and rotation transforms based on pointer movement and velocity.

Customization options

Adjust the following variables at the top of the script to control how strong or smooth the animation feels.

const xyMultiplier       = 30;   // Multiplies pointer velocity for x and y movement
const rotationMultiplier = 20;   // Multiplies torque force to determine rotation speed
const inertiaResistance  = 200;  // Controls how fast the motion eases out (higher = stops quicker)