Dynamic Custom Text Cursor (Edge Aware)

Category: Cursor Animations. Last updated: Aug 20, 2025

Setup — External Scripts

Setup: External Scripts
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>

Code

index.html
html
<div class="cursor">
  <p class="cursor-paragraph">Learn more</p>
</div>
<div class="button-row">
  <a data-cursor="Pretty cool, right?" href="#" class="button">
    <p class="button-text">Custom Cursor</p>
    <div class="button-bg"></div>
  </a>
  <a data-cursor="Attribute based!" href="#" class="button">
    <p class="button-text">With Dynamic Text</p>
    <div class="button-bg"></div>
  </a>
  <a data-cursor="Try the right side of the screen" href="#" class="button">
    <p class="button-text">And Window Edge Awareness</p>
    <div class="button-bg"></div>
  </a>
</div>
styles.css
css
.cursor {
  z-index: 1000;
  opacity: 0;
  pointer-events: none;
  color: #efeeec;
  background-color: #ff4c24;
  border-radius: .25em;
  padding: .3em .75em .4em;
  font-size: 1em;
  transition: opacity .2s;
  position: fixed;
  inset: 0% auto auto 0%;
}

.cursor-paragraph {
  margin-top: 0;
  margin-bottom: 0;
}

.button-row {
  grid-column-gap: .75em;
  grid-row-gap: .75em;
  justify-content: flex-start;
  align-items: center;
  width: 100%;
  padding-left: 1.5em;
  padding-right: 1.5em;
  display: flex;
}

.button {
  color: #131313;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 6em;
  padding-left: 1.5em;
  padding-right: 1.5em;
  font-size: 1em;
  text-decoration: none;
  display: flex;
  position: relative;
}

.button-text {
  z-index: 1;
  margin-top: 0;
  margin-bottom: 0;
  position: relative;
}

.button-bg {
  z-index: 0;
  background-color: #efeeec;
  border-radius: .5em;
  width: 100%;
  height: 100%;
  transition: transform .5s cubic-bezier(.625, .05, 0, 1);
  position: absolute;
  inset: 0%;
}


body:has( [data-cursor]:hover ) .cursor{ opacity: 1; }

.button:hover .button-bg{
  transform: scale(0.95);
}

body:has([data-cursor]:hover) .cursor { 
  opacity: 1; 
}

.button:hover .button-bg{
  transform: scale(0.95);
}
script.js
javascript
function initDynamicCustomTextCursor() {  
  let cursorItem = document.querySelector(".cursor");
  let cursorParagraph = cursorItem.querySelector("p");
  let targets = document.querySelectorAll("[data-cursor]");
  let xOffset = 6;
  let yOffset = 140;
  let cursorIsOnRight = false;
  let currentTarget = null;
  let lastText = '';

  // Position cursor relative to actual cursor position on page load
  gsap.set(cursorItem, { xPercent: xOffset, yPercent: yOffset });

  // Use GSAP quick.to for a more performative tween on the cursor
  let xTo = gsap.quickTo(cursorItem, "x", { ease: "power3" });
  let yTo = gsap.quickTo(cursorItem, "y", { ease: "power3" });

  // Function to get the width of the cursor element including a buffer
  const getCursorEdgeThreshold = () => {
    return cursorItem.offsetWidth + 16; // Cursor width + 16px margin
  };

  // On mousemove, call the quickTo functions to the actual cursor position
  window.addEventListener("mousemove", e => {
    let windowWidth = window.innerWidth;
    let windowHeight = window.innerHeight;
    let scrollY = window.scrollY;
    let cursorX = e.clientX;
    let cursorY = e.clientY + scrollY; // Adjust cursorY to account for scroll

    // Default offsets
    let xPercent = xOffset;
    let yPercent = yOffset;

    // Adjust X offset dynamically based on cursor width
    let cursorEdgeThreshold = getCursorEdgeThreshold();
    if (cursorX > windowWidth - cursorEdgeThreshold) {
      cursorIsOnRight = true;
      xPercent = -100;
    } else {
      cursorIsOnRight = false;
    }

    // Adjust Y offset if in the bottom 10% of the current viewport
    if (cursorY > scrollY + windowHeight * 0.9) {
      yPercent = -120;
    }

    if (currentTarget) {
      let newText = currentTarget.getAttribute("data-cursor");
      if (newText !== lastText) { // Only update if the text is different
        cursorParagraph.innerHTML = newText;
        lastText = newText;

        // Recalculate edge awareness whenever the text changes
        cursorEdgeThreshold = getCursorEdgeThreshold();
      }
    }

    gsap.to(cursorItem, { xPercent: xPercent, yPercent: yPercent, duration: 0.9, ease: "power3" });
    xTo(cursorX);
    yTo(cursorY - scrollY);
  });

  // Add a mouse enter listener for each link that has a data-cursor attribute
  targets.forEach(target => {
    target.addEventListener("mouseenter", () => {
      currentTarget = target; // Set the current target

      let newText = target.getAttribute("data-cursor");

      // Update only if the text changes
      if (newText !== lastText) {
        cursorParagraph.innerHTML = newText;
        lastText = newText;

        // Recalculate edge awareness whenever the text changes
        let cursorEdgeThreshold = getCursorEdgeThreshold();
      }
    });
  });
}

// Initialize Dynamic Text Cursor (Edge Aware)
document.addEventListener('DOMContentLoaded', () => {
  initDynamicCustomTextCursor();
});