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.
Adding the HTML and CSS
We can delete everything in the page.js
, global.css
and page.module.css
and add our own HTML and CSS, to start with a nice blank application.
- We will use Sass for the stylesheets, so we can run
npm i sass
. - We will use Framer Motion for the animation, so we can run
npm i framer-motion
.
Page Component
The parent component will act as the parent of the two main components.
2
import styles from './page.module.scss'
3
import Header from '../components/header';
4
import StickyCursor from '../components/stickyCursor';
6
export default function Home() {
9
<main className={styles.main}>
Header Component
1
import styles from './style.module.scss';
3
export default function Index(props, ref) {
5
<div className={styles.header}>
6
<div className={styles.burger}>
We should have something like this:
Moving the Cursor
Here I'll move the cursor on mouse move with a mix of event listeners and Framer Motion.
1
import StickyCursor from '../components/stickyCursor'
4
export default function Home() {
7
<main className={styles.main}>
To move the cursor, I use two hooks from Framer Motion:
- useMotionValue: a hook that returns an object with an internal state. Allows you to animate without re-rendering the whole components.
- useSpring: a hook that returns a motion value with spring physics applied.
2
import { useEffect } from 'react';
3
import styles from './style.module.scss';
4
import { motion, useMotionValue, useSpring } from 'framer-motion';
6
export default function index({stickyElement}) {
14
const smoothOptions = {damping: 20, stiffness: 300, mass: 0.5}
16
x: useSpring(mouse.x, smoothOptions),
17
y: useSpring(mouse.y, smoothOptions)
20
const manageMouseMove = e => {
21
const { clientX, clientY } = e;
22
mouse.x.set(clientX - cursorSize / 2);
23
mouse.y.set(clientY - cursorSize / 2);
27
window.addEventListener("mousemove", manageMouseMove);
29
window.removeEventListener("mousemove", manageMouseMove)
34
<div className={styles.cursorContainer}>
40
className={styles.cursor}>
We should have something like this:
Sticking the cursor
The first step to stick the cursor is to find a way to pass a ref from a component to another. We can do this using the forwardRef from React.
Here I create a ref in the parent of both components.
2
export default function Home() {
4
const stickyElement = useRef(null);
7
<main className={styles.main}>
8
<Header ref={stickyElement}/>
9
<StickyCursor stickyElement={stickyElement}/>
I use the forwardRef here to pass down the ref coming from the parent.
1
import { forwardRef } from 'react';
2
import styles from './style.module.scss';
3
import Magnetic from '../magnetic';
5
const Header = forwardRef(function index(props, ref) {
7
<div className={styles.header}>
9
<div className={styles.burger}>
10
<div ref={ref} className={styles.bounds}></div>
Then I can use the ref coming from the header and stick the cursor to it.
components/stickyCursor/index.jsx
2
import { useEffect, useState } from 'react';
3
import styles from './style.module.scss';
4
import { motion, useMotionValue, useSpring } from 'framer-motion';
6
export default function index({stickyElement}) {
8
const [isHovered, setIsHovered] = useState(false);
9
const cursorSize = isHovered ? 60 : 15;
17
const smoothOptions = {damping: 20, stiffness: 300, mass: 0.5}
19
x: useSpring(mouse.x, smoothOptions),
20
y: useSpring(mouse.y, smoothOptions)
23
const manageMouseMove = e => {
24
const { clientX, clientY } = e;
25
const { left, top, height, width } = stickyElement.current.getBoundingClientRect();
28
const center = {x: left + width / 2, y: top + height / 2}
33
const distance = {x: clientX - center.x, y: clientY - center.y}
36
mouse.x.set((center.x - cursorSize / 2) + (distance.x * 0.1));
37
mouse.y.set((center.y - cursorSize / 2) + (distance.y * 0.1));
41
mouse.x.set(clientX - cursorSize / 2);
42
mouse.y.set(clientY - cursorSize / 2);
46
const manageMouseOver = e => {
50
const manageMouseLeave = e => {
55
stickyElement.current.addEventListener("mouseenter", manageMouseOver)
56
stickyElement.current.addEventListener("mouseleave", manageMouseLeave)
57
window.addEventListener("mousemove", manageMouseMove);
59
stickyElement.current.removeEventListener("mouseenter", manageMouseOver)
60
stickyElement.current.removeEventListener("mouseleave", manageMouseLeave)
61
window.removeEventListener("mousemove", manageMouseMove)
66
<div className={styles.cursorContainer}>
76
className={styles.cursor}>
We should have something like this:
Stretching the cursor
components/stickyCursor.jsx
2
import { motion, useMotionValue, useSpring, transform, animate } from 'framer-motion';
4
export default function index({stickyElement}) {
11
const manageMouseMove = e => {
12
const { clientX, clientY } = e;
13
const { left, top, height, width } = stickyElement.current.getBoundingClientRect();
16
const center = {x: left + width / 2, y: top + height / 2}
21
const distance = {x: clientX - center.x, y: clientY - center.y}
24
const absDistance = Math.max(Math.abs(distance.x), Math.abs(distance.y));
25
const newScaleX = transform(absDistance, [0, height/2], [1, 1.3])
26
const newScaleY = transform(absDistance, [0, width/2], [1, 0.8])
27
scale.x.set(newScaleX);
28
scale.y.set(newScaleY);
34
const manageMouseLeave = e => {
36
animate(cursor.current, { scaleX: 1, scaleY: 1 }, {duration: 0.1}, { type: "spring" })
40
stickyElement.current.addEventListener("mouseenter", manageMouseOver)
41
stickyElement.current.addEventListener("mouseleave", manageMouseLeave)
44
stickyElement.current.removeEventListener("mouseenter", manageMouseOver)
45
stickyElement.current.removeEventListener("mouseleave", manageMouseLeave)
51
<div className={styles.cursorContainer}>
We should have something like this:
Rotating the cursor
To rotate the cursor, I use the Math.Atan2()
function with the transformTemplate
tag to change the order of the transform property.
We can calculate the necessary angle by using the distance inside the Math.Atan2() function.
components/stickyCursor.jsx
2
import { motion, useMotionValue, useSpring, transform, animate } from 'framer-motion';
4
export default function index({stickyElement}) {
7
const rotate = (distance) => {
8
const angle = Math.atan2(distance.y, distance.x);
9
animate(cursor.current, { rotate: `${angle}rad` }, {duration: 0})
12
const manageMouseMove = e => {
13
const { clientX, clientY } = e;
14
const { left, top, height, width } = stickyElement.current.getBoundingClientRect();
17
const center = {x: left + width / 2, y: top + height / 2}
22
const distance = {x: clientX - center.x, y: clientY - center.y}
30
const template = ({rotate, scaleX, scaleY}) => {
31
return `rotate(${rotate}) scaleX(${scaleX}) scaleY(${scaleY})`
35
<div className={styles.cursorContainer}>
37
transformTemplate={template}
We should have something like this:
Wrapping up
That's it for this animation!
That was a lot of work for a simple animation but it ends being super satisfying, simple and yet quite impressive. Hope you learned a lot!
-Oli