Vue Flow Inspired Animated Cursor
How to create animated cursor with responsive size depending on hovered element.
17/12/2023
Vue Flow Website
While discovering capabilities of Vue Flow I was amazed by the interactive cursor at their website. It felt really fluid and extremely responsive.
Landing page of Vue Flow
Example
Let's build a cursor that has the main capabilities like:
- Eased and delayed movement,
- React to certain elements on hover,
- Change size.
Check it out in action:
Prerequirement
This example is relatively simple to use, we will build it with the following:
The code
Let's start with the cursor HTML element:
<template>
<!-- Change backdrop brightness to invert for Vue Flow look -->
<div
class="
pointer-events-none fixed left-0 top-0 z-50
h-8 w-8 rounded-2xl backdrop-brightness-90
transition-opacity duration-500
"
ref="cursor"
></div>
</template>
Now we have to update the position of the cursor on mouse movement. Let's create a reference to the element, functions to initialize the movement logic and listener to mouse move event.
We will use gsap.to function for moving the div to mouse position. Duration and ease type can be adjusted to your needs.
<script lang="ts" setup>
import gsap from "gsap";
// Cursor reference
const cursor = ref(null);
const mouseX = ref(0);
const mouseY = ref(0);
// Update mouse position on movement
function updateHoverElement(e: MouseEvent) {
mouseX.value = e.clientX;
mouseY.value = e.clientY;
}
// Set up function after Mounted
function setupCursor() {
// Set the anchor point to be in the middle of the cursor element
gsap.set(cursor.value, { xPercent: -50, yPercent: -50 });
// Add listener to mouse move event
window.addEventListener("mousemove", updateHoverElement);
}
watchEffect(() => {
if (cursor.value) {
// Move cursor to mouse position
gsap.to(cursor.value, {
x: mouseX.value,
y: mouseY.value,
duration: 0.6,
ease: "power3",
});
}
});
onMounted(() => {
// Init the cursor
setupCursor();
});
onBeforeUnmount(() => {
// Remove listener after closing the page
window.removeEventListener("mousemove", updateHoverElement);
});
</script>
This code should result in a nice circle following after your pointer.
Now lets move to the part responsible for changing the size of the cursor while hovering on the interactive element:
<script setup lang="ts">
// Rest of the code ...
// Reference to the element you will hover on
const hoveredElement = ref();
function updateHoverElement(e: MouseEvent) {
hoveredElement.value = findParentLink(e.target)?.getBoundingClientRect();
// ...
}
/*
Create a recursive function that returns
the parentElement of hovered node if it includes an <a> tag
*/
const findParentLink = (element: EventTarget | null): HTMLElement | null => {
// Return null if no <a> tag found
if (!element || !(element instanceof HTMLElement)) {
return null;
}
// Check for tags like button or icon if necessary
if (element.tagName.toLowerCase() === "a") {
return element;
} else {
// Continue search
return findParentLink(element.parentElement);
}
};
// ...
watchEffect(() => {
if (cursor.value) {
if (hoveredElement.value) {
// Move to element position
gsap.to(cursor.value, {
x: hoveredElement.value.left + hoveredElement.value.width / 2,
y: hoveredElement.value.top + hoveredElement.value.height / 2,
duration: 0.6,
ease: "power3",
});
// Change to element size
gsap.to(cursor.value, {
width: hoveredElement.value.width + 16,
height: hoveredElement.value.height + 16,
duration: 0.4,
ease: "power4",
});
} else {
gsap.to(cursor.value, {
x: mouseX.value,
y: mouseY.value,
duration: 0.6,
ease: "power3",
});
gsap.to(cursor.value, {
width: 32,
height: 32,
duration: 0.4,
ease: "power4",
});
}
}
});
// ...
</script>
This solution of finding the hoverable elements is not very efficient. Every time you move your cursor, calculation needs to be done in order to check if hovered element is a link tag or a child of one. To make it more efficient, run a querySelector
after page load and fire an event after hovering on the element. You could as well create an array of references in session or localStorage. Code above ensures that if new link is added to the page, it will still be visible to the function. Another performance reducing solution would be limiting the findParentLink() function iterations or applying pointer-events: none
to child element.
Keep in mind
There are many ways to improve how cursor behaves, starting with detecting when the page is scrolled or hiding it when mouse leaves the screen. Hiding the cursor on the page also adds more immersion and design boost.
Conclusion
Dynamic cursors are a neat addition for websites like landing pages or portolio. Keep in mind it could potentially cause performance issues.