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();
});