React animations are usually one of two things, a satisfying joy or a giant pain.
I highly recommend, if you haven’t already, to try to create your own handmade animations from time to time using only javascript and css.
You will learn a lot from it and it will help you along your journey / career as a react developer / frontend developer.
In this react.js tutorial i’ll show you how to make a very easy, yet cool react animation, with no extra packages or libraries needed, only pure javascript, css and react hooks.
React hooks are a new way of managing state and lifecycle events in react, they are not necessarily essential and all logic can be converted for use with the standard react model.
Hooks are truly amazing though, they are a huge time saver, they make your code a lot cleaner and are way more manageable.
If you haven’t used hooks before, and you are interested, I recommend that you have a little read through the official react documentation for hooks.
This will only take you a few minutes, as they explain it very well.
For this tutorial we will create a reusable cursor event based react animation component which works very well with images.
I’m sure you would have seen this before on multiple places, here is an example I put together very quickly.
The animation that we will create will be principally the same as the one I used when I created the animations on the following Telegraph page, (Coming soon).
Let’s start the react.js tutorial, first create a new react project (Make sure create-react-app is up to date).
$ create-react-app my-cool-animation && cd my-cool-animation
Run npm install --save-dev node-sass
so we can make use of sass.
Now add / replace the following packages in your package.json file (This is so we can make use of react hooks):
"dependencies": {
"prop-types": "^15.6.2",
"react": "^16.8.0-alpha.0",
"react-dom": "^16.8.0-alpha.0",
}
Finally run npm install
We should probably create a nice code structure for this project to make it easy to maintain and to be viewed.
I suggest the following for this react.js tutorial (Just create empty directories for the time being):
my-cool-animation
- src
- index.js
- serviceWorker.js
- ui
- components
- assets
- package.json
In the ui
directory, you are going to want to create two files. One called MyAnimation.js
and the other global.scss
Inside of MyAnimation.js
we will setup a very basic react component for the time being, like so:
import React from "react"
import Positional from "../components/positional"
export default function MyAnimation() {
return <Positional />
}
The only thing we are doing here is referencing another react component that we haven’t actually created yet.
Next is to create that component that we have are calling (<Positional />
).
Within the directory src > components
create another directory called positional
then within this directory we will create two files.
The first being index.js
, the second will be our styles.
I will be using css modules here and as long as you are using the latest version of create-react-app
, you will be able to use it as well.
Css modules is as simple as defining your file name as NAME.module.css
, and then importing each class name as if it were an object in javascript. For this react.js tutorial I highly recommend that you try css modules if you haven’t already.
So, create a file called positional.module.scss
(if you are using css modules, if not create any file that will enable you to style our component).
Now you should have a structure that looks like this:
- components
- positional
- index.js
- positional.module.scss
I find that structuring components like this makes it very maintainable and very easy to find and read what you are looking for.
When you import a directory, that has an index.js
file within it, using Javascript, it will automatically import that file.
That is why this structure works.
Now, within our positional > index.js
file lets add in a few lines of code.
import React from "react"
import PropTypes from "prop-types"
import styles from "./positional.module.scss"
import CoordContainer from "./components/coord"
export default function Positional({ children, height, cursorEvent }) {
const ref = React.createRef()
let childrenWithRef
if (Array.isArray(children)) {
childrenWithRef = children.map(child => ({
...child,
props: {
...child.props,
parentRef: ref,
},
}))
} else {
childrenWithRef = {
...children,
props: {
...children.props,
parentRef: ref,
},
}
}
if (!cursorEvent) {
childrenWithRef = children
}
return (
<div
ref={ref}
className={styles.container}
style={height ? { height } : null}
>
<div className={styles.inner}>{childrenWithRef}</div>
</div>
)
}
Positional.propTypes = {
children: PropTypes.any,
height: PropTypes.string,
cursorEvent: PropTypes.bool,
}
Positional.defaultProps = {
children: null,
height: "",
cursorEvent: false,
}
export const Coord = CoordContainer
Okay that was a fair bit of code, let’s break it down and see what is going on.
At the top of the component we are creating a new react ref using const ref = React.createRef();
, doing this will let us access a node within the DOM with great ease.
Terminology made simple
Node = HTML element
DOM = window
You can almost think of it a little like calling document.getElementById()
.
Now we have created that ref we assign it to a DOM element in our JSX, in this instance it will be on our container div element.
Now for us to make use of this react ref we will need to pass it down to the children that will be passed into our <Positional />
component.
It is important to remember that it is a component and therefore should be made reusable.
Lets get onto our first problematic issue, we need to get the ref we just made into each child element that we are passing into the JSX in the form of {props.children}
.
As you can see in the code above, we have created a new variable that is called childrenWithRef
, which we are using to render the child elements passed through props in the JSX.
In that snippet there is a section of code that is using a conditional statement (an if else block) to handle the children prop.
Simply put, this little bit of code will check the type of the prop children
, then add in the ref we created to the props of each child respectively.
Now all children that we pass into our <Positional />
component will have access to it’s parent ref within <Positional />
.
The other bit of code you may be unfamiliar with is className={styles.container}
, this is using css modules to generate a unique class name for that element, the styles in that uniquely generated class name are defined in our positional.module.scss
.
Here is the contents of our positional.module.scss
file:
.container {
width: 100%;
position: relative;
overflow: hidden;
margin: 0 auto;
}
.inner {
composes: container;
max-width: 1200px;
overflow: auto;
position: static;
}
Now we have our parent component finished it is time to create the child components that will be used by our parent component to create that wonderful react positional animation effect.
Let’s create a few more directories following the same structure, only this time we will nest them within the positional directory because they are only to be used by that one component.
Create a structure that mirrors this:
positional
- index.js
- positional.module.scss
- components
- coord
- index.js
- coord.module.scss
This is where we begin to use react hooks for the first time. It’s exactly the same as the react that you know and love, it’s just been improved.
Here is our components > coord > index.js
file
import React, { useState, useEffect } from "react"
import PropTypes from "prop-types"
import { getViewportHeight, getViewportWidth } from "../../../../helpers"
import styles from "./coord.module.scss"
export default function Coord({ children, x, y, parentRef }) {
const [left, setLeft] = useState(0)
const [top, setTop] = useState(0)
if (parentRef) {
const speed = (x * y) / 3 + 50
const viewPortHeight = getViewportHeight()
const viewPortWidth = getViewportWidth()
const mouseMove = e => {
requestAnimationFrame(() => {
const midpointX = viewPortWidth / 2
const midpointY = viewPortHeight / 2
const eventX = e.pageX
const eventY = e.pageY - parentRef.current.offsetTop
const relativeX = (eventX - midpointX) / speed
const relativeY = (eventY - midpointY) / speed
setTop(relativeY)
setLeft(relativeX)
})
}
useEffect(() => {
if (parentRef.current) {
parentRef.current.addEventListener("mousemove", mouseMove)
}
return function cleanup() {
parentRef.current.removeEventListener("mousemove", mouseMove)
}
})
}
return (
<div
className={styles.container}
style={{
left: `${x + -left}%`,
top: `${y + -top}%`,
}}
>
{children}
</div>
)
}
Coord.propTypes = {
children: PropTypes.any,
x: PropTypes.number,
y: PropTypes.number,
parentRef: PropTypes.any,
}
Coord.defaultProps = {
children: null,
x: 50,
y: 50,
parentRef: null,
}
This is where the bulk of the work is done for our animation effect. This component contains all the code we are going to need.
Let’s start by explaining the new side to react that has been brought in with react hooks.
The first thing that has changed is that we are now importing our state and lifecycle events from the react package directly like this: import React, { useState, useEffect } from 'react';
.
In our code we can define a state object by using the useState
function that we have imported.
We do this like the following.
export default function Coord({...}) {
// the next to lines are defining our state.
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);
return (
…
);
};
The function useState()
works a little bit like getters and setters, if you have ever used them.
useState()
also uses array destructuring to assign its variables and functions.
Think of it like this: const [stateVariable, stateVariableSetter] = useState(0)
.
You use this by first defining an initial state for example let’s use the number 0
, this would be useState(0)
, then you will assign two variables one to access this 0 saved in the state, and the other to change the 0 saved in the state, a bit like using this.setState()
.
The equivalent of using lifetime events such as componentDidMount
, componentDidUpdate
, or componentWillUnmount
will now be all contained within one function called useEffect()
.
You use it like the following.
export default function Coord({...}) {
const someFunction = () => {...};
const stopSomeFunction = () => {...};
// you call the function useEffect with a callback that will be run when the component gets
// updated.
useEffect(() => {
someFunction();
// you return a cleanup function which will run when the component will unmount.
return function cleanup() {
stopSomeFunction();
};
});
return (
...
);
};
And that a very basic walk through of react hooks.
Make sure you check out the official React documentation to learn about hooks in depth.
Let’s get back to our animation!
As you can see, the bulk of our animation logic is held within a conditional statement that is checking to see if the parent react ref is ready to be used.
Here is our logic:
const speed = (x * y) / 3 + 50
const viewPortHeight = getViewportHeight()
const viewPortWidth = getViewportWidth()
const mouseMove = e => {
requestAnimationFrame(() => {
const midpointX = viewPortWidth / 2
const midpointY = viewPortHeight / 2
const eventX = e.pageX
const eventY = e.pageY - parentRef.current.offsetTop
const relativeX = (eventX - midpointX) / speed
const relativeY = (eventY - midpointY) / speed
setTop(relativeY)
setLeft(relativeX)
})
}
We are calling two functions here that we have not created yet, don’t worry about these, we will create them later. For the time being just know that they will return the value of the devices viewport height and width.
The requestAnimationFrame()
function just limits the state changes to the frame rate of the browser to create a smooth animation.
The arrow function we are defining as mouseMove
, contains all the logic for our react animation effect. First we need to calculate the center position of our viewport, this will act as our 0, 0 coordinate.
To do this we simply divide the viewport height and width by 2 and save those values as variables that we call midpointX
and midpointY
.
In the next part of our code we grab the current position of the mouse over whole page, this is where our parent react ref comes into play as the whole page can be massive and the midpoint of that would give us bad results.
We have to subtract the offsetTop of our parent ref element from the vertical mouse position (e.pageY
), doing this will give us the mouse position relative to our viewport which makes the component reusable anywhere on a given page and solves those potential errors.
Finally we take our newly calculated mouse position, subtract the 0th coordinate from it, and then divide it by the speed or velocity that we want and then save that to our state variables.
Doing this will ensure that the 0th coordinate will move slower than the coordinates further away!
Feel free to play around with the speed variable to achieve a speed that you want.
If you wanted to, you could pass this in as a prop and control each Coord
component at different speeds.
Next we add our mouseMove
function to the parent element ref with an event that will be triggered any time the user moves their mouse within the parent element.
We do this inside of the useEffect
method like so.
useEffect(() => {
if (parentRef.current) {
parentRef.current.addEventListener("mousemove", mouseMove)
}
return function cleanup() {
parentRef.current.removeEventListener("mousemove", mouseMove)
}
})
Finally we return our JSX with our newly calculated x, y coordinates (in CSS this will be top and left).
<div
className={styles.container}
style={{
left: `${x + -left}%`,
top: `${y + -top}%`,
}}
>
{children}
</div>
Here we are simply, taking an original position as a prop (x
) and subtracting or adding our calculated position from it to provide us with our finished animation!
Let’s add in our css, and the helper methods to calculate the viewport to our project and we will have a working positional react animation component that we can use time and time again to create that awesome effect!
coord.module.scss
.container {
position: absolute;
top: 50%;
left: 50%;
transform: translateY(-50%) translateX(-50%);
}
.span {
display: block;
width: auto;
height: auto;
position: relative;
}
src > helpers > index.js
export function getViewportHeight() {
return window.innerHeight || document.documentElement.clientHeight
}
export function getViewportWidth() {
return window.innerWidth || document.documentElement.clientWidth
}
We are almost done, we have finished our animation component using react hooks, and now it is time to actually use it and test it out.
Head back to our MyAnimation.js
file.
Open up MyAnimation.js
and change a few lines so it matches the following
import React from "react"
import Positional, { Coord } from "../components/positional"
export default function MyAnimation() {
return (
<Positional height="100vh" cursorEvent>
<Coord>
<span>CENTER</span>
</Coord>
<Coord x={10} y={10}>
<span>TOP RIGHT</span>
</Coord>
<Coord x={90} y={90}>
<span>BOTTOM LEFT</span>
</Coord>
</Positional>
)
}
Before we run npm start
make sure to amend your src > index.js
file to use the MyAnimation
component we have made!
src > index.js
import React from "react"
import ReactDOM from "react-dom"
import MyAnimation from "./ui/MyAnimation"
import * as serviceWorker from "./serviceWorker"
ReactDOM.render(<MyAnimation />, document.getElementById("root"))
serviceWorker.unregister()
Now run npm start
and open up your project in your browser and check out the cool animation you just made!
This works really well when you use it with images. You can even add on to this by creating shadows of the children that move at different speeds to the children and so on, be as creative as you like.
That concludes our react.js animation tutorial using react hooks, javascript and css modules!
If you want to just make use of this cool animation, you can install it via npm as I have made it into a npm package that everyone can use.
Just type in npm install --save positional-react-animations
;
Here are some links:
- Npm package: https://www.npmjs.com/package/positional-react-animations
- Github page for package: https://github.com/WillMayger/positional-react-animations
- Github page for full code of this react.js tutorial: https://github.com/WillMayger/positional-react-animation-tutorial-demo
Thanks for reading, if you liked this please like the page and share it with anyone you think would benefit from it.
Will