Look at the results:

Debugging can be done in the TIY editor (here TIY is an online code editor I developed) :

TIY Cursor

You can also preview it on my blog home page.

This mouse pointer style and color scheme comes from a blog I really like. On this basis, I optimize and extend the code to use JS without dependence.


This article is published on my blog, which may be a better reading experience:

How to build a cunning mouse pointer effects | Xecades blog


0x01Setting the entity Pointer

The entity pointer is the dark dot

In order for all elements on the page to have this effect, just set it to *.

The CSS code is as follows:

* {
    cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8px' height='8px'><circle cx='4' cy='4' r='4' opacity='.5'/></svg>") 4 4, auto
}
Copy the code

The URL here is a built-in SVG image, 8 pixels by 8 pixels, with a color of #000 plus 0.5 opacity.

In fact, this step will change later, so we don’t introduce Pointers as a direct CSS file


0x02Generate circles to follow

“Circle” refers to a lighter, larger circle, rendered in DOM.

We enclose the pointer with a class:

class Cursor {... }Copy the code

Create this circle in this class:

if (!this.cursor) {
    this.cursor = document.createElement("div");
    this.cursor.id = "cursor";
    this.cursor.classList.add("hidden"); // The hidden class will be explained later
    document.body.append(this.cursor);
}
Copy the code

Then, we need to determine what mouse actions the user will have:

  1. Move up element: usedocument.onmouseoverimplementation
  2. Remove element: usedocument.onmouseoutimplementation
  3. Move the mouse: usedocument.onmousemoveimplementation
  4. Move to page: usedocument.onmouseenterimplementation
  5. Move off page: usedocument.onmouseleaveimplementation
  6. Press the left button: yesdocument.onmousedownimplementation
  7. Release the left key: usedocument.onmouseupimplementation

Give the circles some styles to use:

#cursor {
    position: fixed;
    width: 16px;
    height: 16px;
    background: # 000;
    border-radius: 8px;
    opacity: 0.25;
    z-index: 10086;
    pointer-events: none;
    transition: 0.2 s ease-in-out;
    transition-property: background, opacity, transform;
}

#cursor.hidden { / * * /
    opacity: 0;
}

#cursor.hover { /* Moves the pointer to a specific element (button, link, etc.) */
    opacity: 0.1;
    transform: scale(2.5);
}

#cursor.active { /* Left mouse button down */
    opacity: 0.5;
    transform: scale(0.5);
}
Copy the code

Then, I say the implementation method of each mouse operation in turn according to the degree of difficulty


Press/release the left key

Two lines of code.

document.onmousedown  = e= > this.cursor.classList.add("active");
document.onmouseup    = e= > this.cursor.classList.remove("active");
Copy the code

Move in/out of the page

(ah… The gray area above is the browser search box. The circle disappears when the mouse moves away.)

document.onmouseenter = e= > this.cursor.classList.remove("hidden");
document.onmouseleave = e= > this.cursor.classList.add("hidden");
Copy the code

Move the element up/out

Let’s start with a concept: what is an element?

Here “element” means “clickable DOM object”.

So how do you teach a computer the difference between clickable and unclickable?

The first thing THAT comes to mind when I think about this is whether the element tag is ,

<span onclick="window.location='/'" style="cursor:pointer">Click ME!</span>
Copy the code

This kind of “button” design is very common in the front end, but the method just didn’t work, so we had to change the idea:

Typically, web designers use a cursor: pointer style on each clickable element to remind the user that “this is a button”, so simply check if the element has a CURSOR: pointer attribute.

Obviously, we can’t use el.style[“cursor”] == pointer because some elements are “inherently” pointer and web designers don’t add additional CSS attributes (such as tags).

The window.getComputedStyle() function (MDN documentation). This function returns the final rendered style of the element.

For compatibility with older browsers, and for some weird errors, I wrote a function like this:

const getStyle = (el, attr) = > {
    try {
        return window.getComputedStyle
            ? window.getComputedStyle(el)[attr]
            : el.currentStyle[attr];
    } catch (e) {}
    return "";
};
Copy the code

GetStyle (el, “cursor”) == “pointer” to determine if the hover effect should exist.


GetStyle () gives me the same property as the global cursor.

Exactly, so we have to do a different approach: after the page is loaded, enumerate each DOM element to see if it meets CURSOR: pointer before changing the global cursor. If so, add it to the list. Then set the global cursor by inserting CSS code with JS.

As a bonus, consider this structure:

<a href="/">
    <div>I'm a block</div>
    <div>I'm also a block</div>
</a>
Copy the code

When you hover over the first

, the browser will assume that the element tag is

Using getStyle(), all elements within the tag have a CURSOR: pointer property, so there is no problem.

Preprocessing part of the code:

var el = document.getElementsByTagName(The '*');
for (let i = 0; i < el.length; i++)
    if (getStyle(el[i], "cursor") = ="pointer")
        this.pt.push(el[i].outerHTML); // pt: pointer(s)
    
this.scr = document.createElement("style");
document.body.appendChild(this.scr);

this.scr.innerHTML = `* {cursor: url("data:image/svg+xml,
      
       
      ") 4 4, auto ! important}`;
Copy the code

The code to finally move the effect on/off:

document.onmouseover  = e= > this.pt.includes(e.target.outerHTML) && this.cursor.classList.add("hover");
document.onmouseout   = e= > this.pt.includes(e.target.outerHTML) && this.cursor.classList.remove("hover");
// Use bit operation here "&&" compress code (and speed up)
Copy the code

Move the mouse

The light-colored circles are always “behind” the dark ones. Here we simply use interpolation to record the position of the previous step, this.pos.prev, and the position of this step, this.pos.curr. We use linear interpolation between them to achieve a lagged effect.

Linear interpolation function:

Math.lerp = (a, b, n) = > (1 - n) * a + n * b;
Copy the code

(Write it in the Math library for good looks. Good kids don’t learn it.)

It returns the ratio n between the numbers a and b.

Note that the cursor default position is the upper left corner of the page. When performing the first cursor move, we do not want the circle to move from the upper left corner to the cursor position.

document.onmousemove = e= > {
    if (this.pos.curr == null)
        this.move(e.clientX - 8, e.clientY - 8); // The move function follows
    this.pos.curr = {x: e.clientX - 8.y: e.clientY - 8};
    this.cursor.classList.remove("hidden");
};
Copy the code

The move() function is used to move cursor:

move(left, top) {
    this.cursor.style["left"] = `${left}px`;
    this.cursor.style["top"] = `${top}px`;
}
Copy the code

At the heart of the entire animation is the render() function:

render() {
    if (this.pos.prev) {
        this.pos.prev.x = Math.lerp(this.pos.prev.x, this.pos.curr.x, 0.15);
        this.pos.prev.y = Math.lerp(this.pos.prev.y, this.pos.curr.y, 0.15);
        this.move(this.pos.prev.x, this.pos.prev.y);
    } else {
        this.pos.prev = this.pos.curr;
    }
    requestAnimationFrame(() = > this.render());
}
Copy the code

To integrate the

Initialize mouse events with init() :

init() {
    document.onmouseover  = e= > this.pt.includes(e.target.outerHTML) && this.cursor.classList.add("hover");
    document.onmouseout   = e= > this.pt.includes(e.target.outerHTML) && this.cursor.classList.remove("hover");
    document.onmousemove  = e= > {(this.pos.curr == null) && this.move(e.clientX - 8, e.clientY - 8); this.pos.curr = {x: e.clientX - 8.y: e.clientY - 8}; this.cursor.classList.remove("hidden"); };document.onmouseenter = e= > this.cursor.classList.remove("hidden");
    document.onmouseleave = e= > this.cursor.classList.add("hidden");
    document.onmousedown  = e= > this.cursor.classList.add("active");
    document.onmouseup    = e= > this.cursor.classList.remove("active");
}
Copy the code

Maintain the crematorium (cross it out)


0x03Add some functionality

In some cases, the page DOM has been re-rendered (such as a vue Router jump) or some elements have been added so that the hover effect does not work. In this case, we need to use refresh() to retrieve the list of hover effects:

refresh() {
    this.scr.remove();
    this.cursor.classList.remove("hover");
    this.cursor.classList.remove("active");
    this.pos = {curr: null.prev: null};
    this.pt = [];

    this.create(); // Generate the desired object (for example, add CSS)
    this.init();   // Initialize the mouse event
    this.render(); // Generate animation
}
Copy the code

0x04The final code

Javascript part:

var CURSOR;

Math.lerp = (a, b, n) = > (1 - n) * a + n * b;

const getStyle = (el, attr) = > {
    try {
        return window.getComputedStyle
            ? window.getComputedStyle(el)[attr]
            : el.currentStyle[attr];
    } catch (e) {}
    return "";
};

class Cursor {
    constructor() {
        this.pos = {curr: null.prev: null};
        this.pt = [];
        this.create();
        this.init();
        this.render();
    }

    move(left, top) {
        this.cursor.style["left"] = `${left}px`;
        this.cursor.style["top"] = `${top}px`;
    }

    create() {
        if (!this.cursor) {
            this.cursor = document.createElement("div");
            this.cursor.id = "cursor";
            this.cursor.classList.add("hidden");
            document.body.append(this.cursor);
        }

        var el = document.getElementsByTagName(The '*');
        for (let i = 0; i < el.length; i++)
            if (getStyle(el[i], "cursor") = ="pointer")
                this.pt.push(el[i].outerHTML);

        document.body.appendChild((this.scr = document.createElement("style")));
        this.scr.innerHTML = `* {cursor: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' width='8px' height='8px'><circle cx='4' cy='4' r='4' opacity='.5'/></svg>") 4 4, auto}`;
    }

    refresh() {
        this.scr.remove();
        this.cursor.classList.remove("hover");
        this.cursor.classList.remove("active");
        this.pos = {curr: null.prev: null};
        this.pt = [];

        this.create();
        this.init();
        this.render();
    }

    init() {
        document.onmouseover  = e= > this.pt.includes(e.target.outerHTML) && this.cursor.classList.add("hover");
        document.onmouseout   = e= > this.pt.includes(e.target.outerHTML) && this.cursor.classList.remove("hover");
        document.onmousemove  = e= > {(this.pos.curr == null) && this.move(e.clientX - 8, e.clientY - 8); this.pos.curr = {x: e.clientX - 8.y: e.clientY - 8}; this.cursor.classList.remove("hidden"); };document.onmouseenter = e= > this.cursor.classList.remove("hidden");
        document.onmouseleave = e= > this.cursor.classList.add("hidden");
        document.onmousedown  = e= > this.cursor.classList.add("active");
        document.onmouseup    = e= > this.cursor.classList.remove("active");
    }

    render() {
        if (this.pos.prev) {
            this.pos.prev.x = Math.lerp(this.pos.prev.x, this.pos.curr.x, 0.15);
            this.pos.prev.y = Math.lerp(this.pos.prev.y, this.pos.curr.y, 0.15);
            this.move(this.pos.prev.x, this.pos.prev.y);
        } else {
            this.pos.prev = this.pos.curr;
        }
        requestAnimationFrame(() = > this.render()); }} (() = > {
    CURSOR = new Cursor();
    // To retrieve the list, use cursor.refresh ()}) ();Copy the code

The CSS part:

#cursor {
    position: fixed;
    width: 16px;
    height: 16px;
    background: # 000;
    border-radius: 8px;
    opacity: 0.25;
    z-index: 10086;
    pointer-events: none;
    transition: 0.2 s ease-in-out;
    transition-property: background, opacity, transform;
}

#cursor.hidden {
    opacity: 0;
}

#cursor.hover {
    opacity: 0.1;
    transform: scale(2.5);
}

#cursor.active {
    opacity: 0.5;
    transform: scale(0.5);
}
Copy the code

This code can be used directly without any consideration of copyright.

Just suggest a comment in the comments section (to enhance my satisfaction as a high school student 😏)