Create a custom cursor

phuocng

Phuoc Nguyen

Posted on January 6, 2024

Create a custom cursor

CSS offers several built-in cursors, including the default arrow, text, and pointer cursors. These cursors can be easily applied to HTML elements using CSS properties like cursor: default, cursor: text, or cursor: pointer. Additionally, you can use custom images as cursors by specifying a URL to an image file using the url() function in CSS.

But if you want to take your website to the next level with a unique and custom cursor, JavaScript is the way to go. With JavaScript, you can create a custom cursor that changes shape or color based on user interaction, or even add animations to it.

In this post, we'll walk you through creating your very own custom cursor using JavaScript. Are you ready to dive in?

Hiding the default cursor

To hide the default cursor, we can set the cursor property of the html element to none. This will make the cursor disappear from the screen. You can change the cursor by setting the CSS property directly like this:

html {
    cursor: none;
}
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use a small piece of JavaScript to set the cursor property like this:

document.documentElement.style.cursor = 'none';
Enter fullscreen mode Exit fullscreen mode

Creating a custom cursor

Once we've hidden the default cursor, we can create a new HTML element (like a div) and position it at the current mouse position on the screen.

To add the new cursor element to the page, we'll use the appendChild() method. This lets us add a new child element to an existing parent element - in our case, we want to add the new cursorEle element as a child of the <body> tag.

const cursorEle = document.createElement('div');
cursorEle.classList.add('cursor');
document.body.appendChild(cursorEle);
Enter fullscreen mode Exit fullscreen mode

In this example, we're going to attach the .cursor class to our custom cursor to define how it looks and behaves. Here's what it looks like:

.cursor {
    position: fixed;
    top: 0;
    left: 0;
    transform: translate(-50%, -50%);
    z-index: 9999;

    width: 0.75rem;
    height: 0.75rem;
    background: rgb(100 116 139);
    border-radius: 50%;
}
Enter fullscreen mode Exit fullscreen mode

To make sure the custom cursor stays in the same place on the screen, even when the user scrolls or moves their mouse, we've set the position of the .cursor element to fixed. We've also used transform: translate(-50%, -50%) to center the custom cursor element on the user's mouse position. This way, we center the element around the mouse coordinates instead of just positioning it with its top-left corner at that point.

To keep the custom cursor visible and prevent it from getting hidden behind other page elements, we assign a high value to the z-index property of the .cursor class. In this example, we've set it to 9999, which ensures that the cursor always stays on top.

Updating the cursor position

To update the position of our custom cursor, we'll handle the mousemove event. This event is triggered whenever the user moves their mouse over an element on the page.

We can add an event listener to the document object that listens for this event and updates the position of our custom cursor element accordingly. Here's some code to help you visualize it:

document.addEventListener('mousemove', (e) => {
    cursorEle.style.top = `${e.clientY}px`;
    cursorEle.style.left = `${e.clientX}px`;
 });
Enter fullscreen mode Exit fullscreen mode

In this code, we use the addEventListener() method to listen for the mousemove event on the document. When this event is triggered, our callback function is called with an object representing the mouse position.

We then use template literals to set the top and left CSS properties of our custom cursor element to match the current mouse coordinates. The clientX and clientY properties of the event object represent the X and Y coordinates of the mouse relative to the top-left corner of the viewport.

By updating these CSS properties in real-time as the user moves their mouse, we can create a custom cursor that follows their movements precisely.

Hiding the cursor when moving out of the document

To avoid the custom cursor element from remaining visible when the user moves their mouse out of the document, we can use the mouseout event to hide it. This event is triggered whenever the user moves their mouse outside of an element on the page.

We can add another event listener to the document object that listens for this event and hides our custom cursor element accordingly.

document.addEventListener('mousemove', (e) => {
    cursorEle.style.display = 'block';
});
document.addEventListener('mouseout', (e) => {
    cursorEle.style.display = 'none';
});
Enter fullscreen mode Exit fullscreen mode

In this block of code, we're hiding our custom cursor element by setting its display CSS property to none whenever the user moves their mouse out of the document. However, we've also made sure to modify the mousemove event handler, so that the cursor element becomes visible again as soon as the user moves their mouse back into the document.

Changing the cursor based on hover interaction

Let's talk about custom cursors. To make them more interactive, we can change their color based on the user's actions. For instance, when hovering over a link or button, we can modify the cursor's appearance to provide visual feedback to the user.

To achieve this effect, we need to add event listeners to the elements we want to track. When the user interacts with these elements (e.g., by hovering over them), we can update the styles of our custom cursor element accordingly.

Check out this example code that modifies the appearance of our custom cursor element when it hovers over a link or button:

const updateCursorOnHover = (ele) => {
    ele.addEventListener('mouseover', () => {
        cursorEle.classList.add('cursor--hover');
    });
    ele.addEventListener('mouseout', () => {
        cursorEle.classList.remove('cursor--hover');
    });
};

document.querySelectorAll('a, button').forEach((ele) => {
    updateCursorOnHover(ele);
});
Enter fullscreen mode Exit fullscreen mode

In this code block, we use the querySelectorAll() method to select all <a> and <button> tags on the page and add event listeners for mouseover and mouseout. When the user hovers over a link or button, our callback function is called, and a custom CSS class, cursor--hover, is added to our custom cursor element. When the user leaves the link, another callback function is called that resets our custom cursor element's styles to its original value by removing the cursor--hover class.

Here's the basic styles for the cursor--hover class:

.cursor--hover {
    mix-blend-mode: difference;
    opacity: 0.75;

    transform: translate(-50%, -50%) scale(3);
    transform-origin: center;
    transition: transform .3s ease-in-out;
}
Enter fullscreen mode Exit fullscreen mode

When the cursor--hover class is added to our custom cursor element, it creates an animation that scales up the size of the cursor and changes its opacity. We achieve this effect by using the transform property with the scale() function to increase the size of the .cursor element.

We also use the opacity property to slightly reduce the visibility of our custom cursor element. Additionally, we set mix-blend-mode: difference, which changes how our custom cursor element blends with other elements on the page. The mix-blend-mode CSS property defines how an element's content should blend with its background or other elements on a page. When set to difference, it causes our custom cursor element to invert colors when it overlaps with another element. This creates a striking visual effect that draws attention to our custom cursor.

Changing the cursor on click

To create a new custom cursor when users click on the page, we can listen for the click event on the document object. When this event occurs, we can create a new HTML element (like a div) and position it at the current mouse coordinates. This will create a new custom cursor that appears when the user clicks.

First, we need to define the styles for our clicked cursor element. Here's an example CSS class:

.cursor--click {
    position: fixed;
    top: 0;
    left: 0;
    transform: translate(-50%, -50%);
    z-index: 9999;

    width: 1rem;
    height: 1rem;

    border: 1px solid rgb(100 116 139);
    border-radius: 50%;

    pointer-events: none;
    animation: pulse .8s ease-in-out;
}
Enter fullscreen mode Exit fullscreen mode

This class creates a small circle element with a pulsing effect that stays in place even when the user scrolls or moves their mouse.

In the CSS code block, we've set the animation property to pulse, which is a CSS animation defined elsewhere in our stylesheet. Here's what the pulse animation looks like:

@keyframes pulse {
    from {
        opacity: 1;
        transform: translate(-50% , -50%) scale(0);
    }
    to {
        opacity: 0;
        transform: translate(-50% , -50%) scale(3);
    }
}
Enter fullscreen mode Exit fullscreen mode

This code creates an animation that runs from the from keyframe to the to keyframe. The properties defined in these two keyframes are then smoothly transitioned over time to create an animation effect.

At the start of the animation (from), our custom cursor element has an initial size and opacity. As time passes, the browser gradually increases the size of our custom cursor element and fades its opacity until it reaches its final size and opacity at the end of the animation (to).

By using this pulsing effect, we can draw attention to our custom cursor element and make it more noticeable when users click on elements on our page.

Next, we need to add an event listener to the document object that listens for the click event. When this event is triggered, we can create a new instance of our clicked cursor element and position it at the current mouse coordinates using template literals.

Here's an example code:

document.addEventListener('click', (event) => {
    const clickedCursor = document.createElement('div');
    clickedCursor.classList.add('cursor--click');
    clickedCursor.style.top = `${e.clientY}px`;
    clickedCursor.style.left = `${e.clientX}px`;

    document.body.appendChild(clickedCursor);
});
Enter fullscreen mode Exit fullscreen mode

In this code block, we're creating a new div element with the document.createElement() method. We're adding the .cursor--click class to the element using classList.add(). To position the cursor at the current mouse coordinates, we're setting its top and left CSS properties. Finally, we're appending the new clicked cursor element to the <body> tag using appendChild().

It's important to note that the cursor is dynamically added to the page whenever users click on the page. So we need to remove the newly created cursor element when it completes the animation. To do that, we can add an event listener to the clicked cursor that listens for the animationend event.

document.addEventListener('click', (e) => {
    // ...
    clickedCursor.addEventListener('animationend', () => {
        clickedCursor.remove();
    });
});
Enter fullscreen mode Exit fullscreen mode

When this event is triggered, our callback function is called and removes the clicked cursor element from the DOM using the remove() method. This ensures that our custom cursors don't accumulate on top of each other and slow down our page.

With this code in place, users will see a new custom cursor every time they click on the page.

Adding custom cursor effects to dynamically created elements

In the previous section, we learned how to change the appearance of the cursor element when users hover over a link or button. But what about elements that are created dynamically? How can we apply the same functionality to them?

Thankfully, we have the MutationObserver API to help us out. This nifty tool allows us to listen for changes in the DOM, even after the initial page load. By using it, we can add event listeners to dynamically created elements and give them the same cool cursor effects as the rest of the page.

Take a look at this example code that uses MutationObserver to add a custom cursor effect when hovering over dynamically created button elements:

// Callback function to execute when mutations are observed
const callback = (mutationsList, observer) => {
    // Loop through all added nodes and check if they are links or buttons
    for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
            const addedNodes = mutation.addedNodes;
            if (addedNodes.length > 0) {
                addedNodes.forEach((node) => {
                    if (node.nodeName === 'A' ||
                        node.nodeName === 'BUTTON'
                    ) {
                        updateCursorOnHover(node);
                    }
                });
            }
        }
    }
};

// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);

// Start observing the target node for configured mutations
observer.observe(document.body, {
    childList: true,
    subtree: true,
});
Enter fullscreen mode Exit fullscreen mode

Our callback function gets triggered every time a mutation happens on the target node. Using a for...of loop, we check if any newly added nodes are either <a> or <button> tags. We do this by looking at their nodeName attribute. If we find any, we add event listeners for mouseover and mouseout by calling the updateCursorOnHover method. These listeners add or remove a custom CSS class to our custom cursor element, creating the desired effect.

To make sure our custom cursor effect is applied to any dynamically created <a> or <button> tags, we create an instance of the MutationObserver class using our callback function and options. We start observing the entire document by calling its observe() method.

We define the options for the observer with { childList: true, subtree: true }. This tells the observer to listen for changes to the child elements of the target node. By doing this, we guarantee that any newly created <a> or <button> tags will automatically get our custom cursor effect applied to them.

Demo

It's time to check out the final result of the steps we've been following so far.


It's highly recommended that you visit the original post to play with the interactive demos.

If you found this series helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

If you want more helpful content like this, feel free to follow me:

💖 💪 🙅 🚩
phuocng
Phuoc Nguyen

Posted on January 6, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related