/

accessorizing the cursor with js

Note: I no longer use this effect on the site. While I liked a lot about it, ultimately the lag of the shadow behind the real cursor made the site feel sluggish.

With iPad OS 13, Apple finally announced mouse and touchpad support. But the way we use a mouse or trackpad is fundamentally different than the way we use touch. To bridge this gap, Apple developed a cursor that snaps to interactable items and adjusts to content. Tech Crunch’s article does a deep dive on it.

Similarly, to highlight interactable items on my site, I wanted to create a custom cursor that would adapt to the content being hovered over. But building a custom cursor for the web has its own set of considerations, chief among them, accessibility.

What’s out there

There are a few ways to modify the cursor on a webpage. CSS even has a built-in way to specify an image as the cursor.

* {
  cursor: url('path-to-image.png') x-cord y-cord, auto;	
}

Unfortunately, it kinda sucks. The image you specify is static and can’t be animated. Not to mention that it removes the existing OS defined cursor, which is a huge hit to accessibility.

In the world of custom JS cursors, the standard practice is to remove the existing cursor with CSS and use JS to draw a new one in its place. Not only is it bad for accessibility, but it’s noticeably more sluggish than the default cursor, making the site feel slower.

My approach

Instead of replacing the cursor altogether, let’s try and add something to it. For this, we’ll need some JS to keep track of the mouse position on the page and move a custom cursor element accordingly.

Within each page of the site we can add our cursor element.

...
<div class="cursor"></div>
...

And we’ll style it as a semi-transparent circle to start.

.cursor {
  display: none; /* starts off hidden */
  width: 25px;
  height: 25px;
  background-color: rgba(0, 0, 0, 0.12);
  border-radius: 13px;
  position: fixed;
  transform: translate(-50%, -50%); /* offsets the div to its center */
  pointer-events: none;
  z-index: 999;
  transition: 0.15s ease; /* makes the custom cursor "follow" the real one */
}

Now for the fun part. With some JS, we can make our custom cursor follow the real one and animate over interactable elements. The code is pretty messy, but I’ve tried to comment it as best as I can.

jQuery(document).ready(function () { // Wait for the DOM to load
    cursor = document.querySelector(".cursor"); // Get the cursor element
    cursor.style.display = "inline-block"; // Make it visible
    cursor.style.width = "25px"; // Set it's starting dimentions
    cursor.style.height = "25px";

    hovered = false; // Create a boolean for whether it's hovering over something clickable
    itemX = 0; // Two values for the position of the clickable element
    itemY = 0;

    window.addEventListener("mousemove", update);
    function update(e) {
        x = e.clientX; // Each time the mouse moves, get its new position
        y = e.clientY;

        if (!hovered) { // If it's not hovering over something, just make it follow the real cursor
            cursor.style.top = y + "px";
            cursor.style.left = x + "px";
        } else { // If it is hovering, snap it to the position of the hovered element
            cursor.style.top = itemY + "px";
            cursor.style.left = itemX + "px";
        }
    }

    Array.from(document.querySelectorAll("a")).forEach((item) => { // Do this for every clickable element (just <a> tags on my site)
        item.addEventListener("mouseenter", function (event) { // Create a listener for when the mouse starts hovering
            itemRect = item.getBoundingClientRect(); // Get the bounding box of the element and find its center
            itemX = itemRect.left + item.offsetWidth / 2.0;
            itemY = itemRect.top + item.offsetHeight / 2.0;
            hovered = true; // Set the hovered flag
            cursor.style.width = item.offsetWidth + 12 + "px"; // Make the cursor the size of the element, plus 12px of padding
            cursor.style.height = item.offsetHeight + 12 + "px";
        }, false);

        item.addEventListener("mouseleave", function (event) {
            hovered = false; // When the mouse leaves, set the flag back and reset the cursor size
            cursor.style.width = "25px";
            cursor.style.height = "25px";
        }, false);
    });

    window.addEventListener('scroll', function () {
        hovered = false; // If the user scrolls, reset everything as well so the cursor doesn't stay big
        cursor.style.width = "25px";
        cursor.style.height = "25px";
    }, true);

    document.addEventListener("mousedown", function (event) { // When the mouse is pressed, make the cursor 8px smaller
        cursor.style.width = parseInt(cursor.style.width, 10) - 8 + "px";
        cursor.style.height = parseInt(cursor.style.height, 10) - 8 + "px";
    }, true);

    document.addEventListener("mouseup", function (event) { // Make it bigger again when the mouse is released
        cursor.style.width = parseInt(cursor.style.width, 10) + 8 + "px";
        cursor.style.height = parseInt(cursor.style.height, 10) + 8 + "px";
    }, false);
});

Last but not least, we can add some CSS to hide the custom cursor on touch devices.

@media (hover: none) {
  .cursor {
    display: none !important;
  }
}

Here’s the final result

If I haven’t grown tired of it, the cursor should still be on my site now (Edit: I grew tired of it). With similar techniques, I’m sure there are endless possibilities for making sites feel more interactable, or just plain fun.