How to create animated cursor with responsive size depending on hovered element.

Vue Flow Inspired Animated Cursor

How to create animated cursor with responsive size depending on hovered element.

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

Vue Flow Main Page Gif Showing Cursor in Action

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:

I am a Link
I am an Icon

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.