profile picture of Olivier Larose

Olivier Larose

March 8, 2024

/

Intermediate

/

Medium

Blend Mode Cursor

How to Make an Animated Cursor using React and GSAP

A website tutorial featuring a moving cursor on mouse move, colored with CSS blend mode difference, made with React and GSAP. Inspired by https://trionn.com/

Live DemoSource code
background video

Initializing the project

Let's start the project by creating a Next.js application. We can do that by running npx create-next-app@latest client inside of a terminal.

  • We will use GSAP for the animation, so we can run npm i gsap.

Tracking the mouse

To track the mouse there would be mutliple ways of doing it. For this tutorial tho, I'll eventually use the requestAnimationFramer to move the cursor around, which is one of the most performance effective way of doing it.

Since I'm doing it that way, I'll store the position of the cursor inside a ref, instead of inside a state and will imperitavely change the styling:

Cursor.jsx

1

'use client';

2

import React, { useEffect, useRef } from 'react'

3

import gsap from 'gsap';

4

5

export default function BlurryCursor() {

6

const mouse = useRef({x: 0, y: 0});

7

const circle = useRef();

8

const size = 30;

9

10

const manageMouseMove = (e) => {

11

const { clientX, clientY } = e;

12

13

mouse.current = {

14

x: clientX,

15

y: clientY

16

}

17

18

moveCircle(mouse.current.x, mouse.current.y);

19

}

20

21

const moveCircle = (x, y) => {

22

gsap.set(circle.current, {x, y, xPercent: -50, yPercent: -50})

23

}

24

25

useEffect( () => {

26

window.addEventListener("mousemove", manageMouseMove);

27

return () => {

28

window.removeEventListener("mousemove", manageMouseMove);

29

}

30

}, [])

31

32

return (

33

<div className='relative h-screen'>

34

<div

35

ref={circle}

36

style={{

37

backgroundColor: "#BCE4F2",

38

width: size,

39

height: size,

40

}}

41

className='top-0 left-0 fixed rounded-full'

42

/>

43

</div>

44

)

45

}
  • Here I'm using the xPercent and yPercent values to make sure the circle of the cursor stays centered

We should have something like this:

Smoothing out the movement

To make the movement of the cursor smoother, we'll use a combination of requestAnimationFrame and a linear interpolation.

Linear Interpolation

Linear interpolation is a key concept in animations. It is often used by motion designers, but we can also use it for web animations! In short, it is form of interpolation, which involves the generation of new values based on an existing set of values

Lerp in Javascript

1

let value = 10;

2

3

const lerp = (x, y, a) => x * (1 - a) + y * a

4

value = lerp(value, 0, 0.1);

5

6

console.log(value)

7

//9
  • x: The value we want to interpolate from (start)
  • y: The target value we want to interpolate to (end)
  • a: The amount by which we want x to be closer to y.

We can now use that function to add a delayedMouse object which we will use to move the circle around:

Cursor.jsx

1

const manageMouseMove = (e) => {

2

const { clientX, clientY } = e;

3

4

mouse.current = {

5

x: clientX,

6

y: clientY

7

}

8

}

9

10

const animate = () => {

11

const { x, y } = delayedMouse.current;

12

13

delayedMouse.current = {

14

x: lerp(x, mouse.current.x, 0.075),

15

y: lerp(y, mouse.current.y, 0.075)

16

}

17

18

moveCircle(delayedMouse.current.x, delayedMouse.current.y);

19

rafId.current = window.requestAnimationFrame(animate);

20

}

21

22

const moveCircle = (x, y) => {

23

gsap.set(circle.current, {x, y, xPercent: -50, yPercent: -50})

24

}
  • Here the position of the mouse is delayed by incrementally moving it towards the cursor at every frame.

We should have something like this:

Adding a hover state

The next thing is to figure out if we're hovering the text, and if so we can start dynamically changing the properties of the cursor.

page.js

1

'use client';

2

import React from 'react'

3

import Cursor from "@/components/Cursor";

4

import { useState } from 'react';

5

6

export default function Scene2() {

7

const [isActive, setIsActive] = useState(false);

8

return (

9

<div className='h-[100vh] flex items-center justify-center'>

10

<h1 onMouseOver={() => {setIsActive(true)}} onMouseLeave={() => {setIsActive(false)}} className="text-[4.5vw] max-w-[90vw] text-center text-white p-20">The quick brown fox jumps over the lazy dog</h1>

11

<Cursor isActive={isActive}/>

12

</div>

13

)

14

}

Cursor.jsx

1

export default function BlurryCursor({isActive}) {

2

...

3

const size = isActive ? 400 : 30;

4

5

useEffect( () => {

6

animate();

7

window.addEventListener("mousemove", manageMouseMove);

8

return () => {

9

window.removeEventListener("mousemove", manageMouseMove);

10

window.cancelAnimationFrame(rafId.current)

11

}

12

}, [isActive])

13

14

return (

15

<div className='relative h-screen'>

16

<div

17

style={{

18

backgroundColor: "#BCE4F2",

19

width: size,

20

height: size,

21

filter: `blur(${isActive ? 30 : 0}px)`,

22

transition: `height 0.3s ease-out, width 0.3s ease-out, filter 0.3s ease-out`

23

}}

24

className='top-0 left-0 fixed rounded-full mix-blend-difference pointer-events-none'

25

ref={circle}

26

/>

27

</div>

28

)

29

}
  • The isActive state is added to the dependencies of the useEffect to effectively re-render the event listeners functions when the isActive state changes
  • The mix-blend-mode difference is added directly to the circle

We should have something like this:

Adding multiple circles

This is a variation of the animation, where we add multiple colored circles to make them blend together using the mix blend mode.

  • Here nothing changes, but I'm returning 4 circles instead of a single one.

Cursor.jsx

1

...

2

3

const colors = [

4

"#c32d27",

5

"#f5c63f",

6

"#457ec4",

7

"#356fdb",

8

]

9

10

const circles = useRef([]);

11

12

const moveCircles = (x, y) => {

13

if(circles.current.length < 1) return;

14

circles.current.forEach((circle, i) => {

15

gsap.set(circle, {x, y, xPercent: -50, yPercent: -50})

16

})

17

}

18

19

return (

20

<div className='relative h-screen'>

21

{

22

[...Array(4)].map((_, i) => {

23

return (

24

<div

25

style={{

26

backgroundColor: colors[i],

27

width: size,

28

height: size,

29

filter: `blur(${isActive ? 20 : 2}px)`,

30

transition: `transform ${(4 - i) * delay}s linear, height 0.3s ease-out, width 0.3s ease-out, filter 0.3s ease-out`

31

}}

32

className='top-0 left-0 fixed rounded-full mix-blend-difference'

33

key={i}

34

ref={ref => circles.current[i] = ref}

35

/>)

36

})

37

}

38

</div>

39

)

Note:

  • each circle has a different transition value for the transform property to make the movement slightly delayed.
  • To not make the text affected by the mix blend mode, I have given it a z-index of 50, to put it on top of all the circles

We should have something like this:

Wrapping up

As simple as this!

Hope you liked the animation, it's crazy what we can create with simple css properties and an animated cursor. Hope you learned something!

-Oli

Related Animations

image

June 2, 2024

Mouse Image Distortion

A website animation featuring an image distortion in a curved, using the sin function, React, React Three Fiber and Framer Motion

image

May 4, 2024

Paint Reveal

A website tutorial on making a paint reveal / erasing effect using the destination out blend mode of the canvas API, made with React and Next.js

image

March 8, 2024

Creative Buttons

A website tutorial featuring animated buttons taken from awwwards winning websites. Remade using React and SASS. Inspired by https://timestwo.design, https://hello.cuberto.com/ and https://lusion.co/