Reveal animations on scroll with react-spring
Table of contents
In this post, you’ll use the react-spring library to create reveal animations on scroll. I’m talking about simple animations like fade-ins and slide-ups. To determine if an element is visible, you’ll use the react-visibility-sensor. I will also mention some smaller libraries you can use that specialize in reveal animations—instead of react-spring
—and I will show a solution with react-visibility-sensor and CSS transitions.
Related libraries
There are many libraries that specialize in scroll animations. A popular is aos at about 4.5kb, and another one is sal.js, which is under 1kb.
On the other hand, react-visibility-sensor
is about 2.9kb, and react-spring
is around 10kb. Besides that, neither of them has good tree-shaking. This means that you’ll end up with an additional 13kb in your final bundle size, no matter how much code you use. In fact, I tested this, and from the initial Create React App to the final version of this example, the bundle size increased by 14kb.
As a result, this example is useful if you’re already using react-spring for animations in your app. If you don’t, sal.js
might be a better alternative. In the following link, you can find an example react component with sal.js
The plan
You can create repeatable animations with the Spring
component from react-spring
. While skimming through the documentation, I thought you can use it only for mounting animations. You specify a from
and a to
prop, and the component animates when first mounts on the screen. But later I noticed that the from
prop is optional. Only the to
prop is required.
But what will help us here is that you can change the to
prop during runtime and the component will animate accordingly.
So, the plan is to:
- Use
react-visibility-sensor
to determine if the element is visible or not, - Change the
to
prop (or more specifically, the opacity) of theSpring
component during runtime, as shown below:
<Spring to={{ opacity: isVisible ? 1 : 0 }} />
Project Setup
GitHub repo with the completed example
The first thing you have to do is to create a new project with Create React App:
npx create-react-app react-spring-scroll
cd react-spring-scroll
Then, install the dependencies, and start the development server:
yarn add react-spring react-visibility-sensor
yarn start
After that, go to the src/App.js
file, and add the following code. Pay attention to the highlighted lines:
import React from "react";
import { Spring } from "react-spring/renderprops";
import VisibilitySensor from "react-visibility-sensor";
// styles
const centeredStyles = {
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
height: "100%",
};
const h2Styles = {
fontSize: "82px",
};
const App = () => {
return (
<div>
<div
style={{
...centeredStyles,
height: "100vh",
backgroundColor: "pink",
}}
>
<VisibilitySensor>
{({ isVisible }) => (
<Spring delay={300} to={{ opacity: isVisible ? 1 : 0 }}>
{({ opacity }) => (
<h2 style={{ ...h2Styles, opacity }}>Hello</h2>
)}
</Spring>
)}
</VisibilitySensor>
</div>
</div>
);
};
export default App;
In the code above, we use two components. The Spring
and the VisibilitySensor
. They are both render prop components—meaning that they expose their functionality through function parameters. For example, the VisibilitySensor
does this through the isVisible
, and the Spring
through the opacity
.
From VisibilitySensor
we use the isVisible
boolean flag. We pass no other props to the component.
In the Spring
component, we pass 2 props.
- We pass a
delay
with a value of 300ms to give some time to the animation. We do that in case the page hasn’t loaded yet, or if there’s some delay with the scroll event. - We pass the
to
prop. If the component (heading 2) is visible, we pass to theto
prop an object with an opacity property of1
. If it’s not visible, we change it to0
. Whenever that value changes, the component fades in or fades out accordingly.
To see the effect better, you’ll add now 2 more sections. Those sections will have slightly different reveal animations. Again pay attention to the highlighted lines:
import React from "react";
import { Spring } from "react-spring/renderprops";
import VisibilitySensor from "react-visibility-sensor";
// skipping styles...
const App = () => {
return (
<div>
{/* skipping first section... */}
{/* copy from here... */}
<div
style={{
...centeredStyles,
overflow: "hidden",
height: "100vh",
}}
>
<VisibilitySensor partialVisibility>
{({ isVisible }) => (
<Spring
delay={300}
to={{
opacity: isVisible ? 1 : 0,
transform: isVisible
? "translateX(0)"
: "translateX(200px)",
}}
>
{(props) => (
<h2 style={{ ...h2Styles, ...props }}>World</h2>
)}
</Spring>
)}
</VisibilitySensor>
</div>
<div
style={{
...centeredStyles,
height: "100vh",
overflow: "hidden",
backgroundColor: "#afd4d4",
}}
>
<VisibilitySensor partialVisibility offset={{ bottom: -400 }}>
{({ isVisible }) => (
<Spring
delay={300}
to={{
opacity: isVisible ? 1 : 0,
transform: isVisible
? "translateY(0)"
: "translateY(400px)",
}}
>
{(props) => <h2 style={{ ...h2Styles, ...props }}>!!!</h2>}
</Spring>
)}
</VisibilitySensor>
</div>
{/* until here... */}
</div>
);
};
export default App;
We’re doing almost the same thing. But instead of just fading in and out the elements we’re sliding them to the left in the “World” section and sliding them up in the “!!!” section. The overflow: "hidden"
on the containers is there because we don’t want the heading elements expanding (overflowing) the page when we place them in their start position (for example, translateX(200px)
). Be careful with the overflow
, though; displaying the content is more important than some fancy animations.
I also added to every VisibilitySensor
component the partialVisibility
prop. By having that prop set to true, the following happens:
- The component will become visible if a part of it is visible
- The component will hide if all of it is out of the view.
By default, the partialVisibility
is set to false. In this case, the behavior is the complete opposite of the above.
To see this clearly, add these lines to the h2Styles
variable:
const h2Styles = {
fontSize: "82px",
color: "white",
backgroundColor: "black",
padding: "16px 32px",
};
If you now scroll up and down, you will notice something. The animations occur every time the elements become visible. In the next section, you’ll see how to animate them only once.
Enhance VisibilitySensor
If you want to animate the elements only when they first become visible, you’ll have to override the default component from react-visibility-sensor
and implement an once
prop. That prop will allow you to turn off the sensor after the first reveal. Create the VisibilitySensor.jsx
component inside src/components
:
import React, { Component } from "react";
import PropTypes from "prop-types";
import VSensor from "react-visibility-sensor";
class VisibilitySensor extends Component {
constructor(props) {
super(props);
this.state = {
active: true,
};
}
render() {
const { active } = this.state;
const { once, children, ...theRest } = this.props;
return (
<VSensor
active={active}
onChange={(isVisible) =>
once &&
isVisible &&
this.setState({ active: false }, () =>
console.log("turned the thing off!")
)
}
{...theRest}
>
{({ isVisible }) => children({ isVisible })}
</VSensor>
);
}
}
VisibilitySensor.propTypes = {
once: PropTypes.bool,
children: PropTypes.func.isRequired,
};
VisibilitySensor.defaultProps = {
once: false,
};
export default VisibilitySensor;
We keep an active
boolean flag in the component state. When the element first comes into view, and the once
prop is true, we change the active
state to false. By doing that, we turn off the sensor by passing to the active
prop a value of false
.
To use this, go back to your App.js
and do the following:
- Change the import of the
VisibilitySensor
from the node package to the custom component. - Add an
once
prop as shown below:
import VisibilitySensor from "./components/VisibilitySensor";
<VisibilitySensor once>
Now I’ll show you another solution that uses the VisibilitySensor
and CSS transitions.
A solution with CSS transitions
You saw earlier that if you don’t want to use react-spring
, you can instead use some smaller libraries that specialize in reveal animations. Now I’ll show you how to do reveal animations with CSS transitions and react-visibility-sensor
.
The plan here is to swap the classes of the h2
elements (visible/hidden), and these classes, in turn, will trigger some CSS transitions. I choose transitions over animations because you can easily revert them—if the element changes state (visible/hidden) without completing the animation. That’s assuming you want repeated reveal animations, and you don’t want to use the once
prop from VisibilitySensor
.
Let’s start with the App.js
component:
// skipping the other imports.
import "./App.css";
const App = () => {
return (
<div>
<div
style={{
...centeredStyles,
height: "100vh",
backgroundColor: "pink",
}}
>
<VisibilitySensor>
{({ isVisible }) => (
<h2
className={isVisible ? "fadeIn enter" : "fadeIn"}
style={{ ...h2Styles }}
>
Hello
</h2>
)}
</VisibilitySensor>
</div>
<div
style={{
...centeredStyles,
height: "100vh",
overflow: "hidden",
}}
>
<VisibilitySensor partialVisibility>
{({ isVisible }) => (
<h2
className={isVisible ? "slideLeft enter" : "slideLeft"}
style={{ ...h2Styles }}
>
World
</h2>
)}
</VisibilitySensor>
</div>
<div
style={{
...centeredStyles,
height: "100vh",
backgroundColor: "#afd4d4",
overflow: "hidden",
}}
>
<VisibilitySensor partialVisibility offset={{ bottom: -400 }}>
{({ isVisible }) => (
<h2
className={isVisible ? "slideUp enter" : "slideUp"}
style={{ ...h2Styles }}
>
!!!
</h2>
)}
</VisibilitySensor>
</div>
</div>
);
};
After that, you’ll have to write some CSS for those classes. Replace the contents of your App.css
with the following:
.fadeIn {
transition: opacity 1s ease-in-out;
opacity: 0;
}
.fadeIn.enter {
transition: opacity 1s ease-in-out;
opacity: 1;
}
.slideLeft {
transition: opacity 1s ease-in-out, transform 1s ease-in-out;
opacity: 0;
transform: translateX(100%);
}
.slideLeft.enter {
transition: opacity 1s ease-in-out, transform 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
opacity: 1;
transform: translateX(0);
}
.slideUp {
transition: opacity 1s ease-in-out, transform 1s ease-in-out;
opacity: 0;
transform: translateY(100%);
}
.slideUp.enter {
transition: opacity 1s ease-in-out, transform 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
opacity: 1;
transform: translateY(0);
}
@media (print), (prefers-reduced-motion: reduce) {
.fadeIn,
.slideLeft,
.slideUp {
transition: none;
opacity: 1;
transform: none;
}
}
At the end of the file, there is a prefers-reduced-motion media query that disables the animations if the user wishes so. They can enable this option from their operating system settings.
The last thing I want to show you is how to implement reveal animations if you use Server Side Rendering (SSR).
SSR with JavaScript disabled
The solutions you saw earlier work well for a client-side application with create-react-app
. If you try to do the same in an application that does some SSR—let’s take, for example, Gatsby—you’ll see that we have the following problem. If you load the page with JavaScript disabled, your content will be hidden because the elements start with opacity: 0
.
To solve this, you want to disable all reveal animations by default, and if JavaScript is available, you want to enable them again. You can do this by adding a no-js
class to your body
element and then writing some CSS that disables the animations when the no-js
class is present. After that, you add a script
, right after the body
element, that will remove the no-js
class from the body if JavaScript is enabled.
Let’s see how you can do that in Gatsby for the solution with the CSS transitions. Here’s a GitHub repo for the example in Gatsby if you want to take a look at the code.
In Gatsby, you can add a class in the body element with the setBodyAttributes
method that’s available in the onRenderBody SSR API. Create a gatsby-ssr.js
file at the root of the project and implement the onRenderBody
method:
export const onRenderBody = ({ setBodyAttributes }) => {
setBodyAttributes({ className: "no-js" });
};
Next, you have to write the CSS that will disable the animations when that class is present:
body.no-js .fadeIn {
opacity: 1;
}
body.no-js .slideLeft {
opacity: 1;
transform: none;
}
body.no-js .slideUp {
opacity: 1;
transform: none;
}
The last thing you have to do is add a script
tag right after the body
element. You can do this in Gatsby with the setPreBodyComponents
method that’s available as a parameter of the onRenderBody
API. As a result, you’ll have to change your gatsby-ssr.js
to the following:
import React from "react";
export const onRenderBody = ({
setPreBodyComponents,
setBodyAttributes,
}) => {
setBodyAttributes({ className: "no-js" });
setPreBodyComponents([
<script
dangerouslySetInnerHTML={{
__html: `document.querySelector('body').classList.remove('no-js')`,
}}
/>,
]);
};
You can achieve the same result in the react-spring
example by adding a .reveal
class in the h2
elements and writing the following CSS:
body.no-js .reveal {
opacity: 1 !important;
transform: none !important;
}
Keep an eye for react-spring
version 9 because it will have a method you can use to disable animations.
Final Words
Let’s summarize now some key points you saw throughout this post:
- You can determine if an element is visible with
react-visibility-sensor
. - You can use that information to animate elements with
react-spring
or with CSS transitions. - You saw how to adjust the
VisibilitySensor
to fire only once to avoid repeatable reveal animations. - You can use some lightweight libraries that specialize in reveal animations instead of a heavier library like
react-spring
. I also gave a GitHub link to an example React component withsal.js
. - Finally, you saw how to implement the above in a client-side app with
create-react-app
and in an ”SSR” app with Gatsby.
Other things to read
Popular
- Reveal animations on scroll with react-spring
- Gatsby background image example
- Extremely fast loading with Gatsby and self-hosted fonts