Custom cursors are everywhere on inspirational sites like Awwwards. Dots that trail your movement, change color, or scale when you hover over elements. When inspecting these implementations, I often notice how much JavaScript is involved. Like always I was curious if it was possible to create this effect with as little javaScript as possible. Of course there’s some javaScript needed to get the mouse position and apply it to the custom cursor. But with CSS custom properties it’s possible to pass the mouse coordinates as variables and let CSS do the rest. So what about mouse interactions such as hover or active? Well, with the :has pseudo-class it’s possible to check if the cursor is hovering over any element in the document and apply different styles to the custom cursor.

The :has pseudo-class is mostly referred to as the “parent selector” because it allows you to select a parent element based on its children. In most cases it’s used to select a parent based on its direct children or descendants. But the implications of this selector are much greater. By chaining selectors together it’s possible to select elements based on other elements that are nowhere near each other. The trick is to go completely up the document tree and down again. That’s why it is sometimes referred to as the “god selector”.

The basic setup is to add an empty <div class="cursor"></div> element at the end of the body. This makes sure the cursor is always on top of other elements by default. You could do this step with javascript but why not just add it to the HTML directly?

The javascript is pretty simple. All that’s needed is the mouse position while moving and pass the coordinates as CSS custom properties. If you want a smoother effect you need to add a delay and use requestAnimationFrame. I’m adding px to the end of the values to make sure they can be used as CSS values. But you could even do that in CSS. This is the basic version:

document.addEventListener("mousemove", (e) => {
  document.documentElement.style.setProperty("--mx", e.clientX + "px");
  document.documentElement.style.setProperty("--my", e.clientY + "px");
});

So now the 2 custom properties --mx and --my are added to the document root. Since the custom properties update when the mouse moves, the cursor will follow the mouse when we apply them as variables for the value of the translate property of the cursor.

.cursor {
  --cursor-size: 20px;
  width: var(--cursor-size);
  aspect-ratio: 1;
  border-radius: 50%;
  background-color: indigo;
  position: fixed;
  top: 0;
  left: 0;
  /* use translate to substract the size directly */
  translate: calc(var(--mx) - var(--cursor-size) / 2) calc(
      var(--my) - var(--cursor-size) / 2
    );
  transition: 0.25s ease;
  transition-property: scale, opacity;
  pointer-events: none;
}

I’m also using custom properties to set the width and height of the cursor. This makes it easy to change the size of the cursor in one place. And I can use the same property to position the cursor in the middle of the cursor by using calc to subtract half the width and height from the mouse position.

And now it’s time to use the :has pseudo-class to check if the document has any hovered or active links or buttons. If it does, we can change the size and opacity of the cursor to create a hover effect.

body:has(:is(a:hover, button:hover)) .cursor {
  scale: 2;
  opacity: 0.75;
}

body:has(:is(a:active, button:active)) .cursor {
  scale: 1.5;
  opacity: 0.75;
}

Basic demo

Link

That’s all there is to it. You can of course add more interactions and styles to the cursor. For example changing the color based on the hovered element or adding different effects for different types of elements. The possibilities are endless.

Advanced demo

In this case I updated the cursor’s internal HTML with multiple possible states. Based on the hovered element different parts of the cursor are shown or hidden. This way I can show text inside the cursor or show an icon based on the hovered element.

body:has(.cursor-read:hover) .cursor__pointer--read {
  opacity: 1;
}

Have a look at this codepen to see the full code and play around with it.

View

Hi, I'm Simon

I'm a designer and frontend developer crafting web interfaces since 2007. These days, I teach design and frontend development at Howest University in Kortrijk, Belgium. On this blog, I'm going to share my thoughts and insights on all things web. I also make things on Codepen and share stuff on Bluesky.

Articles

  1. Custom cursor interactions with :has

  2. A CSS-only fluid typography approach

  3. A Long Overdue Hello World

More coming soon…